@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
@@ -258,32 +258,39 @@ export function taskComposerMoveVertical(
258
258
 
259
259
  export function taskComposerVisibleLines(
260
260
  buffer: TaskComposerBuffer,
261
- cursorToken = '_',
261
+ cursorToken = '',
262
+ cursorVisible = true,
262
263
  ): readonly string[] {
263
264
  const normalized = normalizeTaskComposerBuffer(buffer);
265
+ if (!cursorVisible) {
266
+ return normalized.text.split('\n');
267
+ }
264
268
  const textWithCursor =
265
- normalized.text.slice(0, normalized.cursor) +
266
- cursorToken +
267
- normalized.text.slice(normalized.cursor);
269
+ normalized.cursor >= normalized.text.length
270
+ ? `${normalized.text}${cursorToken}`
271
+ : normalized.text.slice(0, normalized.cursor) +
272
+ cursorToken +
273
+ normalized.text.slice(normalized.cursor + 1);
268
274
  return textWithCursor.split('\n');
269
275
  }
270
276
 
271
- export function taskComposerTextFromTaskFields(title: string, description: string): string {
272
- if (description.length === 0) {
273
- return title;
277
+ export function taskComposerTextFromTaskFields(title: string, body: string): string {
278
+ if (body.length > 0) {
279
+ return body;
274
280
  }
275
- return `${title}\n${description}`;
281
+ return title;
276
282
  }
277
283
 
278
284
  export function taskFieldsFromComposerText(text: string): {
279
- readonly title: string;
280
- readonly description: string;
285
+ readonly title: string | null;
286
+ readonly body: string;
281
287
  } {
282
- const lines = text.split('\n');
288
+ const normalized = text.replace(/\r\n/gu, '\n');
289
+ const lines = normalized.split('\n');
283
290
  const firstLine = lines[0] ?? '';
284
- const rest = lines.slice(1).join('\n');
291
+ const title = firstLine.trim();
285
292
  return {
286
- title: firstLine.trim(),
287
- description: rest,
293
+ title: title.length === 0 ? null : title,
294
+ body: normalized,
288
295
  };
289
296
  }
@@ -1,11 +1,11 @@
1
1
  import { padOrTrimDisplay } from './dual-pane-core.ts';
2
- import { type TaskStatus } from './harness-core-ui.ts';
3
- import { formatUiButton } from '../ui/kit.ts';
4
- import {
5
- taskComposerTextFromTaskFields,
6
- taskComposerVisibleLines,
7
- type TaskComposerBuffer,
8
- } from './task-composer.ts';
2
+ import type { TaskStatus } from './harness-core-ui.ts';
3
+ import { UiKit } from '../../packages/harness-ui/src/kit.ts';
4
+ import { WrappingInputRenderer } from '../../packages/harness-ui/src/text-layout.ts';
5
+ import { taskComposerTextFromTaskFields, type TaskComposerBuffer } from './task-composer.ts';
6
+
7
+ const UI_KIT = new UiKit();
8
+ const WRAPPING_INPUT_RENDERER = new WrappingInputRenderer();
9
9
 
10
10
  export type TaskFocusedPaneAction =
11
11
  | 'repository.dropdown.toggle'
@@ -24,6 +24,7 @@ interface ActionCell {
24
24
  export interface TaskFocusedPaneRepositoryRecord {
25
25
  readonly repositoryId: string;
26
26
  readonly name: string;
27
+ readonly metadata?: Record<string, unknown>;
27
28
  readonly archivedAt: string | null;
28
29
  }
29
30
 
@@ -31,7 +32,7 @@ export interface TaskFocusedPaneTaskRecord {
31
32
  readonly taskId: string;
32
33
  readonly repositoryId: string | null;
33
34
  readonly title: string;
34
- readonly description: string;
35
+ readonly body: string;
35
36
  readonly status: TaskStatus;
36
37
  readonly orderIndex: number;
37
38
  readonly createdAt: string;
@@ -58,6 +59,7 @@ interface BuildTaskFocusedPaneOptions {
58
59
  readonly cols: number;
59
60
  readonly rows: number;
60
61
  readonly scrollTop: number;
62
+ readonly cursorVisible?: boolean;
61
63
  }
62
64
 
63
65
  interface PaneLine {
@@ -70,6 +72,7 @@ interface PaneLine {
70
72
 
71
73
  export interface TaskFocusedPaneView {
72
74
  readonly rows: readonly string[];
75
+ readonly plainRows?: readonly string[];
73
76
  readonly taskIds: readonly (string | null)[];
74
77
  readonly repositoryIds: readonly (string | null)[];
75
78
  readonly actions: readonly (TaskFocusedPaneAction | null)[];
@@ -78,52 +81,71 @@ export interface TaskFocusedPaneView {
78
81
  readonly selectedRepositoryId: string | null;
79
82
  }
80
83
 
81
- const READY_CHIP_LABEL = formatUiButton({
82
- label: 'ready',
83
- prefixIcon: 'r',
84
- });
85
- const DRAFT_CHIP_LABEL = formatUiButton({
86
- label: 'queued',
87
- prefixIcon: 'd',
88
- });
89
- const COMPLETE_CHIP_LABEL = formatUiButton({
90
- label: 'complete',
91
- prefixIcon: 'c',
92
- });
93
-
94
84
  function sortedRepositories(
95
85
  repositories: ReadonlyMap<string, TaskFocusedPaneRepositoryRecord>,
96
86
  ): readonly TaskFocusedPaneRepositoryRecord[] {
97
- return [...repositories.values()]
98
- .filter((entry) => entry.archivedAt === null)
99
- .sort(
100
- (left, right) =>
101
- left.name.localeCompare(right.name) || left.repositoryId.localeCompare(right.repositoryId),
102
- );
87
+ return [...repositories.values()].filter((repository) => repository.archivedAt === null);
103
88
  }
104
89
 
105
90
  function parseIsoMs(value: string): number {
106
91
  return Date.parse(value);
107
92
  }
108
93
 
109
- function sortTasksByOrderLocal(
94
+ function taskStatusSortRank(status: TaskStatus): number {
95
+ if (status === 'in-progress') {
96
+ return 0;
97
+ }
98
+ if (status === 'ready') {
99
+ return 1;
100
+ }
101
+ if (status === 'draft') {
102
+ return 2;
103
+ }
104
+ return 3;
105
+ }
106
+
107
+ function taskStatusGroupLabel(status: TaskStatus): string {
108
+ if (status === 'in-progress') {
109
+ return 'in prog';
110
+ }
111
+ if (status === 'ready') {
112
+ return 'ready';
113
+ }
114
+ if (status === 'draft') {
115
+ return 'draft';
116
+ }
117
+ return 'complete';
118
+ }
119
+
120
+ function compareTasksByOrder(
121
+ left: TaskFocusedPaneTaskRecord,
122
+ right: TaskFocusedPaneTaskRecord,
123
+ ): number {
124
+ if (left.orderIndex !== right.orderIndex) {
125
+ return left.orderIndex - right.orderIndex;
126
+ }
127
+ const leftTs = parseIsoMs(left.createdAt);
128
+ const rightTs = parseIsoMs(right.createdAt);
129
+ const leftFinite = Number.isFinite(leftTs);
130
+ const rightFinite = Number.isFinite(rightTs);
131
+ if (leftFinite && rightFinite && leftTs !== rightTs) {
132
+ return leftTs - rightTs;
133
+ }
134
+ if (leftFinite !== rightFinite) {
135
+ return leftFinite ? -1 : 1;
136
+ }
137
+ return left.taskId.localeCompare(right.taskId);
138
+ }
139
+
140
+ function sortTasksForDisplay(
110
141
  tasks: readonly TaskFocusedPaneTaskRecord[],
111
142
  ): readonly TaskFocusedPaneTaskRecord[] {
112
143
  return [...tasks].sort((left, right) => {
113
- if (left.orderIndex !== right.orderIndex) {
114
- return left.orderIndex - right.orderIndex;
144
+ const statusCompare = taskStatusSortRank(left.status) - taskStatusSortRank(right.status);
145
+ if (statusCompare !== 0) {
146
+ return statusCompare;
115
147
  }
116
- const leftTs = parseIsoMs(left.createdAt);
117
- const rightTs = parseIsoMs(right.createdAt);
118
- const leftFinite = Number.isFinite(leftTs);
119
- const rightFinite = Number.isFinite(rightTs);
120
- if (leftFinite && rightFinite && leftTs !== rightTs) {
121
- return leftTs - rightTs;
122
- }
123
- if (leftFinite !== rightFinite) {
124
- return leftFinite ? -1 : 1;
125
- }
126
- return left.taskId.localeCompare(right.taskId);
148
+ return compareTasksByOrder(left, right);
127
149
  });
128
150
  }
129
151
 
@@ -151,63 +173,26 @@ function truncate(text: string, max: number): string {
151
173
  return `${text.slice(0, safeMax - 1)}…`;
152
174
  }
153
175
 
154
- function composeRowWithRightChips(
155
- left: string,
156
- width: number,
157
- chips: readonly { label: string; action: TaskFocusedPaneAction }[],
158
- ): { readonly text: string; readonly cells: readonly ActionCell[] } {
159
- const joined = chips.map((chip) => chip.label).join(' ');
160
- if (joined.length === 0 || joined.length >= width) {
161
- return {
162
- text: padOrTrimDisplay(left, width),
163
- cells: [],
164
- };
165
- }
166
- const startCol = Math.max(0, width - joined.length);
167
- const leftMax = Math.max(0, startCol - 1);
168
- const leftText = padOrTrimDisplay(truncate(left, leftMax), leftMax);
169
- const gap = width - leftText.length - joined.length;
170
- let cursor = leftText.length + Math.max(0, gap);
171
- const cells: ActionCell[] = [];
172
- const parts: string[] = [leftText, ' '.repeat(Math.max(0, gap))];
173
- for (let idx = 0; idx < chips.length; idx += 1) {
174
- const chip = chips[idx]!;
175
- parts.push(chip.label);
176
- cells.push({
177
- startCol: cursor,
178
- endCol: cursor + chip.label.length - 1,
179
- action: chip.action,
180
- });
181
- cursor += chip.label.length;
182
- if (idx < chips.length - 1) {
183
- parts.push(' ');
184
- cursor += 1;
185
- }
186
- }
187
- return {
188
- text: padOrTrimDisplay(parts.join(''), width),
189
- cells,
190
- };
191
- }
192
-
193
176
  function taskBufferFromRecord(
194
177
  task: TaskFocusedPaneTaskRecord,
195
178
  overrides: ReadonlyMap<string, TaskComposerBuffer>,
196
179
  ): TaskComposerBuffer {
180
+ const text = taskComposerTextFromTaskFields(task.title, task.body);
197
181
  return (
198
182
  overrides.get(task.taskId) ?? {
199
- text: taskComposerTextFromTaskFields(task.title, task.description),
200
- cursor: task.title.length,
183
+ text,
184
+ cursor: text.length,
201
185
  }
202
186
  );
203
187
  }
204
188
 
205
189
  function taskPreviewText(task: TaskFocusedPaneTaskRecord): string {
206
- const summary = task.description.split('\n')[0] ?? '';
207
- if (summary.length === 0) {
208
- return task.title;
190
+ const summary = task.body.split('\n')[0] ?? '';
191
+ const trimmed = summary.trim();
192
+ if (trimmed.length > 0) {
193
+ return trimmed;
209
194
  }
210
- return `${task.title} · ${summary}`;
195
+ return task.title;
211
196
  }
212
197
 
213
198
  export function buildTaskFocusedPaneView(
@@ -224,7 +209,7 @@ export function buildTaskFocusedPaneView(
224
209
  repositories[0]?.repositoryId ??
225
210
  null;
226
211
 
227
- const scopedTasks = sortTasksByOrderLocal(
212
+ const scopedTasks = sortTasksForDisplay(
228
213
  [...options.tasks.values()].filter((task) => task.repositoryId === selectedRepositoryId),
229
214
  );
230
215
 
@@ -251,7 +236,7 @@ export function buildTaskFocusedPaneView(
251
236
  ? 'select repository'
252
237
  : (repositories.find((entry) => entry.repositoryId === selectedRepositoryId)?.name ??
253
238
  '(missing)');
254
- const repositoryButton = formatUiButton({
239
+ const repositoryButton = UI_KIT.formatButton({
255
240
  label: truncate(selectedRepositoryName, Math.max(8, safeCols - 16)),
256
241
  suffixIcon: 'v',
257
242
  });
@@ -291,53 +276,68 @@ export function buildTaskFocusedPaneView(
291
276
  } else if (scopedTasks.length === 0) {
292
277
  push(' no tasks yet for this repository');
293
278
  } else {
279
+ const statusCounts = new Map<TaskStatus, number>();
280
+ for (const task of scopedTasks) {
281
+ statusCounts.set(task.status, (statusCounts.get(task.status) ?? 0) + 1);
282
+ }
294
283
  push(` tasks (${String(scopedTasks.length)})`);
284
+ let previousStatus: TaskStatus | null = null;
295
285
  for (let index = 0; index < scopedTasks.length; index += 1) {
296
286
  const task = scopedTasks[index]!;
287
+ if (task.status !== previousStatus) {
288
+ if (previousStatus !== null) {
289
+ push('');
290
+ }
291
+ push(
292
+ ` ${statusGlyph(task.status)} ${taskStatusGroupLabel(task.status)} · ${String(statusCounts.get(task.status) ?? 0)}`,
293
+ );
294
+ previousStatus = task.status;
295
+ }
297
296
  const focused =
298
297
  options.editorTarget.kind === 'task' && options.editorTarget.taskId === task.taskId;
299
- const leftLabel = ` ${focused ? '▸' : ' '} ${statusGlyph(task.status)} ${truncate(taskPreviewText(task), Math.max(8, safeCols - 24))}`;
300
- const chips =
301
- task.status === 'completed'
302
- ? []
303
- : [
304
- { label: READY_CHIP_LABEL, action: 'task.status.ready' as const },
305
- { label: DRAFT_CHIP_LABEL, action: 'task.status.draft' as const },
306
- { label: COMPLETE_CHIP_LABEL, action: 'task.status.complete' as const },
307
- ];
308
- const composed = composeRowWithRightChips(leftLabel, safeCols, chips);
309
- push(composed.text, task.taskId, selectedRepositoryId, 'task.focus', composed.cells);
310
-
311
298
  if (focused) {
312
299
  const editBuffer = taskBufferFromRecord(task, options.taskBufferById);
313
- const linesWithCursor = taskComposerVisibleLines(editBuffer);
314
- push(
315
- ` ${padOrTrimDisplay('─'.repeat(Math.max(4, Math.min(20, safeCols - 4))), Math.max(0, safeCols - 4))}`,
316
- );
300
+ const editorInnerWidth = Math.max(1, safeCols - 4);
301
+ const editorPrefix = `${statusGlyph(task.status)} `;
302
+ const linesWithCursor = WRAPPING_INPUT_RENDERER.renderLines({
303
+ buffer: editBuffer,
304
+ width: editorInnerWidth,
305
+ linePrefix: editorPrefix,
306
+ cursorToken: '█',
307
+ cursorVisible: options.cursorVisible ?? true,
308
+ });
309
+ push(` ┌${'─'.repeat(editorInnerWidth)}┐`);
317
310
  for (const line of linesWithCursor) {
318
- push(
319
- ` ${truncate(line, Math.max(1, safeCols - 4))}`,
320
- task.taskId,
321
- selectedRepositoryId,
322
- 'task.focus',
323
- );
311
+ const content = padOrTrimDisplay(line, editorInnerWidth);
312
+ push(` │${content}│`, task.taskId, selectedRepositoryId, 'task.focus');
324
313
  }
314
+ push(` └${'─'.repeat(editorInnerWidth)}┘`);
315
+ continue;
325
316
  }
317
+
318
+ const leftLabel = ` ${statusGlyph(task.status)} ${truncate(taskPreviewText(task), Math.max(8, safeCols - 6))}`;
319
+ push(leftLabel, task.taskId, selectedRepositoryId, 'task.focus');
326
320
  }
327
321
  }
328
322
 
329
323
  push('');
330
324
  const draftFocused = options.editorTarget.kind === 'draft';
331
325
  push(` draft ${draftFocused ? '(editing)' : '(saved)'}`);
332
- const draftLines = draftFocused
333
- ? taskComposerVisibleLines(options.draftBuffer)
334
- : options.draftBuffer.text.length === 0
335
- ? ['']
336
- : options.draftBuffer.text.split('\n');
326
+ const draftInnerWidth = Math.max(1, safeCols - 4);
327
+ push(` ┌${'─'.repeat(draftInnerWidth)}┐`);
328
+ const draftLines = WRAPPING_INPUT_RENDERER.renderLines({
329
+ buffer: options.draftBuffer,
330
+ width: draftInnerWidth,
331
+ cursorToken: '█',
332
+ cursorVisible: draftFocused && (options.cursorVisible ?? true),
333
+ });
337
334
  for (const line of draftLines) {
338
- push(` > ${truncate(line, Math.max(1, safeCols - 3))}`);
335
+ const content = padOrTrimDisplay(line, draftInnerWidth);
336
+ push(` │${content}│`);
339
337
  }
340
- push(' enter ready tab queue shift+enter newline ctrl+g repos');
338
+ push(` └${'─'.repeat(draftInnerWidth)}┘`);
339
+ push(' enter ready tab draft shift+enter newline');
340
+ push(' alt+g repos ctrl+up/down reorder');
341
341
 
342
342
  const maxTop = Math.max(0, lines.length - safeRows);
343
343
  const top = Math.max(0, Math.min(maxTop, options.scrollTop));
@@ -353,6 +353,7 @@ export function buildTaskFocusedPaneView(
353
353
  }
354
354
  return {
355
355
  rows: viewport.map((line) => line.text),
356
+ plainRows: viewport.map((line) => line.text),
356
357
  taskIds: viewport.map((line) => line.taskId),
357
358
  repositoryIds: viewport.map((line) => line.repositoryId),
358
359
  actions: viewport.map((line) => line.action),
@@ -1,3 +1,12 @@
1
+ import {
2
+ DEFAULT_TASK_SCREEN_KEYBINDINGS_RAW,
3
+ TASK_SCREEN_KEYBINDING_ACTION_ORDER,
4
+ type TaskScreenKeybindingAction,
5
+ } from './keybinding-registry.ts';
6
+
7
+ export { DEFAULT_TASK_SCREEN_KEYBINDINGS_RAW };
8
+ export type { TaskScreenKeybindingAction };
9
+
1
10
  interface KeyStroke {
2
11
  readonly key: string;
3
12
  readonly ctrl: boolean;
@@ -11,87 +20,7 @@ interface ParsedBinding {
11
20
  readonly originalText: string;
12
21
  }
13
22
 
14
- export type TaskScreenKeybindingAction =
15
- | 'mux.home.repo.dropdown.toggle'
16
- | 'mux.home.repo.next'
17
- | 'mux.home.repo.previous'
18
- | 'mux.home.task.submit'
19
- | 'mux.home.task.queue'
20
- | 'mux.home.task.newline'
21
- | 'mux.home.task.status.ready'
22
- | 'mux.home.task.status.draft'
23
- | 'mux.home.task.status.complete'
24
- | 'mux.home.task.reorder.up'
25
- | 'mux.home.task.reorder.down'
26
- | 'mux.home.editor.cursor.left'
27
- | 'mux.home.editor.cursor.right'
28
- | 'mux.home.editor.cursor.up'
29
- | 'mux.home.editor.cursor.down'
30
- | 'mux.home.editor.line.start'
31
- | 'mux.home.editor.line.end'
32
- | 'mux.home.editor.word.left'
33
- | 'mux.home.editor.word.right'
34
- | 'mux.home.editor.delete.backward'
35
- | 'mux.home.editor.delete.forward'
36
- | 'mux.home.editor.delete.word.backward'
37
- | 'mux.home.editor.delete.line.start'
38
- | 'mux.home.editor.delete.line.end';
39
-
40
- const ACTION_ORDER: readonly TaskScreenKeybindingAction[] = [
41
- 'mux.home.repo.dropdown.toggle',
42
- 'mux.home.repo.next',
43
- 'mux.home.repo.previous',
44
- 'mux.home.task.submit',
45
- 'mux.home.task.queue',
46
- 'mux.home.task.newline',
47
- 'mux.home.task.status.ready',
48
- 'mux.home.task.status.draft',
49
- 'mux.home.task.status.complete',
50
- 'mux.home.task.reorder.up',
51
- 'mux.home.task.reorder.down',
52
- 'mux.home.editor.cursor.left',
53
- 'mux.home.editor.cursor.right',
54
- 'mux.home.editor.cursor.up',
55
- 'mux.home.editor.cursor.down',
56
- 'mux.home.editor.line.start',
57
- 'mux.home.editor.line.end',
58
- 'mux.home.editor.word.left',
59
- 'mux.home.editor.word.right',
60
- 'mux.home.editor.delete.backward',
61
- 'mux.home.editor.delete.forward',
62
- 'mux.home.editor.delete.word.backward',
63
- 'mux.home.editor.delete.line.start',
64
- 'mux.home.editor.delete.line.end',
65
- ] as const;
66
-
67
- export const DEFAULT_TASK_SCREEN_KEYBINDINGS_RAW: Readonly<
68
- Record<TaskScreenKeybindingAction, readonly string[]>
69
- > = {
70
- 'mux.home.repo.dropdown.toggle': ['ctrl+g'],
71
- 'mux.home.repo.next': ['ctrl+n'],
72
- 'mux.home.repo.previous': ['ctrl+p'],
73
- 'mux.home.task.submit': ['enter'],
74
- 'mux.home.task.queue': ['tab'],
75
- 'mux.home.task.newline': ['shift+enter'],
76
- 'mux.home.task.status.ready': ['alt+r'],
77
- 'mux.home.task.status.draft': ['alt+d'],
78
- 'mux.home.task.status.complete': ['alt+c'],
79
- 'mux.home.task.reorder.up': ['alt+up'],
80
- 'mux.home.task.reorder.down': ['alt+down'],
81
- 'mux.home.editor.cursor.left': ['left', 'ctrl+b'],
82
- 'mux.home.editor.cursor.right': ['right', 'ctrl+f'],
83
- 'mux.home.editor.cursor.up': ['up'],
84
- 'mux.home.editor.cursor.down': ['down'],
85
- 'mux.home.editor.line.start': ['ctrl+a', 'home'],
86
- 'mux.home.editor.line.end': ['ctrl+e', 'end'],
87
- 'mux.home.editor.word.left': ['alt+b'],
88
- 'mux.home.editor.word.right': ['alt+f'],
89
- 'mux.home.editor.delete.backward': ['backspace'],
90
- 'mux.home.editor.delete.forward': ['delete'],
91
- 'mux.home.editor.delete.word.backward': ['ctrl+w', 'alt+backspace'],
92
- 'mux.home.editor.delete.line.start': ['ctrl+u'],
93
- 'mux.home.editor.delete.line.end': ['ctrl+k'],
94
- };
23
+ const ACTION_ORDER = TASK_SCREEN_KEYBINDING_ACTION_ORDER;
95
24
 
96
25
  const KEY_TOKEN_ALIASES = new Map<string, string>([
97
26
  ['cmd', 'meta'],
@@ -437,10 +366,18 @@ function parseBinding(input: string): ParsedBinding | null {
437
366
 
438
367
  function bindingsForAction(raw: readonly string[]): readonly ParsedBinding[] {
439
368
  const parsed: ParsedBinding[] = [];
369
+
370
+ const pushIfUnique = (candidate: ParsedBinding): void => {
371
+ if (parsed.some((existing) => strokesEqual(existing.stroke, candidate.stroke))) {
372
+ return;
373
+ }
374
+ parsed.push(candidate);
375
+ };
376
+
440
377
  for (const value of raw) {
441
378
  const next = parseBinding(value);
442
379
  if (next !== null) {
443
- parsed.push(next);
380
+ pushIfUnique(next);
444
381
  }
445
382
  }
446
383
  return parsed;