@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,10 +1,16 @@
1
1
  import type { ConversationRailSessionSummary } from './conversation-rail.ts';
2
- import { formatUiButton } from '../ui/kit.ts';
2
+ import type {
3
+ ProjectPaneGitHubReviewSummary,
4
+ ProjectPaneGitHubReviewThread,
5
+ } from './project-pane-github-review.ts';
6
+ import { UiKit } from '../../packages/harness-ui/src/kit.ts';
3
7
  import type {
4
8
  StreamSessionController,
5
9
  StreamSessionDisplayPhase,
6
10
  } from '../control-plane/stream-protocol.ts';
7
11
 
12
+ const UI_KIT = new UiKit();
13
+
8
14
  interface WorkspaceRailGitSummary {
9
15
  readonly branch: string;
10
16
  readonly changedFiles: number;
@@ -61,17 +67,25 @@ interface WorkspaceRailModel {
61
67
  readonly directories: readonly WorkspaceRailDirectorySummary[];
62
68
  readonly conversations: readonly WorkspaceRailConversationSummary[];
63
69
  readonly processes: readonly WorkspaceRailProcessSummary[];
70
+ readonly showGitHubIntegration?: boolean;
71
+ readonly visibleGitHubDirectoryKeys?: ReadonlySet<string> | readonly string[];
72
+ readonly expandedGitHubDirectoryKeys?: ReadonlySet<string> | readonly string[];
73
+ readonly githubReviewByDirectoryKey?: ReadonlyMap<string, ProjectPaneGitHubReviewSummary>;
64
74
  readonly showTaskPlanningUi?: boolean;
75
+ readonly showNimEntry?: boolean;
76
+ readonly showTasksEntry?: boolean;
65
77
  readonly activeProjectId: string | null;
78
+ readonly activeGitHubProjectId?: string | null;
66
79
  readonly activeRepositoryId?: string | null;
67
80
  readonly activeConversationId: string | null;
68
81
  readonly projectSelectionEnabled?: boolean;
82
+ readonly githubSelectionEnabled?: boolean;
69
83
  readonly repositorySelectionEnabled?: boolean;
70
84
  readonly homeSelectionEnabled?: boolean;
85
+ readonly nimSelectionEnabled?: boolean;
86
+ readonly tasksSelectionEnabled?: boolean;
71
87
  readonly repositoriesCollapsed?: boolean;
72
88
  readonly collapsedRepositoryGroupIds?: readonly string[];
73
- readonly shortcutHint?: string;
74
- readonly shortcutsCollapsed?: boolean;
75
89
  readonly nowMs?: number;
76
90
  }
77
91
 
@@ -83,10 +97,10 @@ interface WorkspaceRailViewRow {
83
97
  | 'conversation-body'
84
98
  | 'process-title'
85
99
  | 'process-meta'
100
+ | 'github-header'
101
+ | 'github-detail'
86
102
  | 'repository-header'
87
103
  | 'repository-row'
88
- | 'shortcut-header'
89
- | 'shortcut-body'
90
104
  | 'action'
91
105
  | 'muted';
92
106
  readonly text: string;
@@ -100,7 +114,7 @@ interface WorkspaceRailViewRow {
100
114
 
101
115
  const NEW_THREAD_INLINE_LABEL = '[+ thread]';
102
116
  const UNTRACKED_REPOSITORY_GROUP_ID = 'untracked';
103
- const ADD_PROJECT_BUTTON_LABEL = formatUiButton({
117
+ const ADD_PROJECT_BUTTON_LABEL = UI_KIT.formatButton({
104
118
  label: 'add project',
105
119
  prefixIcon: '>',
106
120
  });
@@ -110,8 +124,11 @@ type WorkspaceRailAction =
110
124
  | 'conversation.delete'
111
125
  | 'project.add'
112
126
  | 'home.open'
127
+ | 'nim.open'
128
+ | 'tasks.open'
113
129
  | 'project.close'
114
- | 'shortcuts.toggle'
130
+ | 'project.github.open'
131
+ | 'project.github.toggle'
115
132
  | 'repository.toggle'
116
133
  | 'repository.add'
117
134
  | 'repository.edit'
@@ -243,6 +260,60 @@ function trackedProjectGitSuffix(git: WorkspaceRailGitSummary): string {
243
260
  return ` (${git.branch}:+${String(git.additions)},-${String(git.deletions)})`;
244
261
  }
245
262
 
263
+ function githubReviewCommentCount(threads: readonly ProjectPaneGitHubReviewThread[]): number {
264
+ let total = 0;
265
+ for (const thread of threads) {
266
+ total += thread.comments.length;
267
+ }
268
+ return total;
269
+ }
270
+
271
+ function githubPrLifecycleLabel(pr: NonNullable<ProjectPaneGitHubReviewSummary['pr']>): string {
272
+ if (pr.isDraft || pr.state === 'draft') {
273
+ return 'draft';
274
+ }
275
+ if (pr.state === 'merged') {
276
+ return 'merged';
277
+ }
278
+ if (pr.state === 'closed') {
279
+ return 'closed';
280
+ }
281
+ return 'open';
282
+ }
283
+
284
+ function githubRailSummarySuffix(review: ProjectPaneGitHubReviewSummary | null): string {
285
+ if (review === null) {
286
+ return '(not loaded)';
287
+ }
288
+ if (review.status === 'loading') {
289
+ return '(loading)';
290
+ }
291
+ if (review.status === 'error') {
292
+ return '(error)';
293
+ }
294
+ if (review.pr === null) {
295
+ return '(no pr)';
296
+ }
297
+ const unresolvedCommentCount = githubReviewCommentCount(review.openThreads);
298
+ const detailParts = [
299
+ `#${String(review.pr.number)} ${githubPrLifecycleLabel(review.pr)}`,
300
+ `unresolved ${String(unresolvedCommentCount)}`,
301
+ ];
302
+ if (review.pr.ciRollup === 'failure') {
303
+ detailParts.push('ci failed');
304
+ }
305
+ return `(${detailParts.join(', ')})`;
306
+ }
307
+
308
+ function sanitizeInlineText(value: string): string {
309
+ return value.replace(/\s+/gu, ' ').trim();
310
+ }
311
+
312
+ function formatAuthor(login: string | null): string {
313
+ const normalized = login?.trim() ?? '';
314
+ return normalized.length > 0 ? `@${normalized}` : '@unknown';
315
+ }
316
+
246
317
  function conversationDisplayTitle(conversation: WorkspaceRailConversationSummary): string {
247
318
  const title = conversation.title.trim();
248
319
  if (title.length === 0) {
@@ -280,8 +351,29 @@ function buildContentRows(
280
351
  ): readonly WorkspaceRailViewRow[] {
281
352
  const rows: WorkspaceRailViewRow[] = [];
282
353
  const showTaskPlanningUi = model.showTaskPlanningUi ?? true;
354
+ const showNimEntry = model.showNimEntry ?? true;
355
+ const showGitHubIntegration = model.showGitHubIntegration ?? false;
356
+ const visibleGitHubDirectoryKeys =
357
+ model.visibleGitHubDirectoryKeys === undefined
358
+ ? new Set<string>()
359
+ : model.visibleGitHubDirectoryKeys instanceof Set
360
+ ? model.visibleGitHubDirectoryKeys
361
+ : new Set(model.visibleGitHubDirectoryKeys);
362
+ const expandedGitHubDirectoryKeys =
363
+ model.expandedGitHubDirectoryKeys === undefined
364
+ ? new Set<string>()
365
+ : model.expandedGitHubDirectoryKeys instanceof Set
366
+ ? model.expandedGitHubDirectoryKeys
367
+ : new Set(model.expandedGitHubDirectoryKeys);
368
+ const githubReviewByDirectoryKey =
369
+ model.githubReviewByDirectoryKey ?? new Map<string, ProjectPaneGitHubReviewSummary>();
370
+ const showTasksEntry = model.showTasksEntry ?? showTaskPlanningUi;
283
371
  const homeSelectionEnabled = model.homeSelectionEnabled ?? false;
372
+ const nimSelectionEnabled = model.nimSelectionEnabled ?? false;
373
+ const tasksSelectionEnabled = model.tasksSelectionEnabled ?? false;
284
374
  const projectSelectionEnabled = model.projectSelectionEnabled ?? false;
375
+ const githubSelectionEnabled = model.githubSelectionEnabled ?? false;
376
+ const activeGitHubProjectId = model.activeGitHubProjectId ?? null;
285
377
  const repositorySelectionEnabled = model.repositorySelectionEnabled ?? false;
286
378
  const collapsedRepositoryGroupIds = new Set(model.collapsedRepositoryGroupIds ?? []);
287
379
  const repositoryById = new Map(
@@ -367,6 +459,21 @@ function buildContentRows(
367
459
 
368
460
  if (showTaskPlanningUi) {
369
461
  pushRow(rows, 'dir-header', '├─ 🏠 home', homeSelectionEnabled, null, null, null, 'home.open');
462
+ if (showNimEntry) {
463
+ pushRow(rows, 'dir-header', '├─ 🦎 nim', nimSelectionEnabled, null, null, null, 'nim.open');
464
+ }
465
+ if (showTasksEntry) {
466
+ pushRow(
467
+ rows,
468
+ 'dir-header',
469
+ '├─ 🗂️ tasks',
470
+ tasksSelectionEnabled,
471
+ null,
472
+ null,
473
+ null,
474
+ 'tasks.open',
475
+ );
476
+ }
370
477
  }
371
478
 
372
479
  if (orderedRepositoryGroupIds.length === 0) {
@@ -424,6 +531,132 @@ function buildContentRows(
424
531
  const conversations = model.conversations.filter(
425
532
  (conversation) => conversation.directoryKey === directory.key,
426
533
  );
534
+ const processes = model.processes.filter((process) => process.directoryKey === directory.key);
535
+
536
+ const githubVisibleForDirectory =
537
+ visibleGitHubDirectoryKeys.has(directory.key) ||
538
+ (githubSelectionEnabled && directory.key === activeGitHubProjectId);
539
+ if (showGitHubIntegration && group.tracked && githubVisibleForDirectory) {
540
+ const githubReview = githubReviewByDirectoryKey.get(directory.key) ?? null;
541
+ const githubSelected = githubSelectionEnabled && directory.key === activeGitHubProjectId;
542
+ const githubExpanded = expandedGitHubDirectoryKeys.has(directory.key);
543
+ const githubTreePrefix = `${projectChildPrefix}├─ `;
544
+ const githubDetailPrefix = `${projectChildPrefix}│ `;
545
+ pushRow(
546
+ rows,
547
+ 'github-header',
548
+ `${githubTreePrefix}${githubExpanded ? '▼' : '▶'} github pr ${githubRailSummarySuffix(
549
+ githubReview,
550
+ )}`,
551
+ githubSelected,
552
+ null,
553
+ directory.key,
554
+ repositoryId,
555
+ 'project.github.open',
556
+ );
557
+ if (githubExpanded) {
558
+ if (githubReview === null) {
559
+ pushRow(
560
+ rows,
561
+ 'github-detail',
562
+ `${githubDetailPrefix}status not loaded`,
563
+ githubSelected,
564
+ null,
565
+ directory.key,
566
+ repositoryId,
567
+ null,
568
+ );
569
+ } else if (githubReview.status === 'loading') {
570
+ pushRow(
571
+ rows,
572
+ 'github-detail',
573
+ `${githubDetailPrefix}status loading GitHub review data…`,
574
+ githubSelected,
575
+ null,
576
+ directory.key,
577
+ repositoryId,
578
+ null,
579
+ );
580
+ } else if (githubReview.status === 'error') {
581
+ const message =
582
+ githubReview.errorMessage === null
583
+ ? 'unknown error'
584
+ : sanitizeInlineText(githubReview.errorMessage);
585
+ pushRow(
586
+ rows,
587
+ 'github-detail',
588
+ `${githubDetailPrefix}status error ${message}`,
589
+ githubSelected,
590
+ null,
591
+ directory.key,
592
+ repositoryId,
593
+ null,
594
+ );
595
+ } else if (githubReview.pr === null) {
596
+ const branchName = githubReview.branchName?.trim() ?? '';
597
+ pushRow(
598
+ rows,
599
+ 'github-detail',
600
+ `${githubDetailPrefix}branch ${branchName.length > 0 ? branchName : '(none)'}`,
601
+ githubSelected,
602
+ null,
603
+ directory.key,
604
+ repositoryId,
605
+ null,
606
+ );
607
+ pushRow(
608
+ rows,
609
+ 'github-detail',
610
+ `${githubDetailPrefix}no pull request for tracked branch`,
611
+ githubSelected,
612
+ null,
613
+ directory.key,
614
+ repositoryId,
615
+ null,
616
+ );
617
+ } else {
618
+ const pr = githubReview.pr;
619
+ pushRow(
620
+ rows,
621
+ 'github-detail',
622
+ `${githubDetailPrefix}pr #${String(pr.number)} ${githubPrLifecycleLabel(pr)} ${sanitizeInlineText(pr.title)}`,
623
+ githubSelected,
624
+ null,
625
+ directory.key,
626
+ repositoryId,
627
+ null,
628
+ );
629
+ pushRow(
630
+ rows,
631
+ 'github-detail',
632
+ `${githubDetailPrefix}from ${pr.headBranch} -> ${pr.baseBranch} by ${formatAuthor(
633
+ pr.authorLogin,
634
+ )}`,
635
+ githubSelected,
636
+ null,
637
+ directory.key,
638
+ repositoryId,
639
+ null,
640
+ );
641
+ pushRow(
642
+ rows,
643
+ 'github-detail',
644
+ `${githubDetailPrefix}threads ${String(githubReview.openThreads.length)} open / ${String(
645
+ githubReview.resolvedThreads.length,
646
+ )} resolved (${String(
647
+ githubReviewCommentCount(githubReview.openThreads) +
648
+ githubReviewCommentCount(githubReview.resolvedThreads),
649
+ )} comments)`,
650
+ githubSelected,
651
+ null,
652
+ directory.key,
653
+ repositoryId,
654
+ null,
655
+ );
656
+ }
657
+ }
658
+ }
659
+
427
660
  for (
428
661
  let conversationIndex = 0;
429
662
  conversationIndex < conversations.length;
@@ -434,6 +667,8 @@ function buildContentRows(
434
667
  const active =
435
668
  !projectSelectionEnabled &&
436
669
  !homeSelectionEnabled &&
670
+ !nimSelectionEnabled &&
671
+ !tasksSelectionEnabled &&
437
672
  !repositorySelectionEnabled &&
438
673
  conversation.sessionId === model.activeConversationId;
439
674
  const projection = projectWorkspaceRailConversation(conversation, {
@@ -472,8 +707,6 @@ function buildContentRows(
472
707
  );
473
708
  }
474
709
  }
475
-
476
- const processes = model.processes.filter((process) => process.directoryKey === directory.key);
477
710
  for (const process of processes) {
478
711
  pushRow(
479
712
  rows,
@@ -502,70 +735,6 @@ function buildContentRows(
502
735
  return rows;
503
736
  }
504
737
 
505
- function shortcutDescriptionRows(shortcutHint: string | undefined): readonly string[] {
506
- const normalized = shortcutHint?.trim();
507
- if (normalized === undefined || normalized.length === 0) {
508
- return [
509
- 'ctrl+t new thread',
510
- 'ctrl+g critique thread',
511
- 'ctrl+x archive thread',
512
- 'ctrl+l take over thread',
513
- 'ctrl+o add project',
514
- 'ctrl+w close project',
515
- 'ctrl+j/k switch nav',
516
- '→ expand repo',
517
- '← collapse repo',
518
- 'ctrl+k ctrl+j expand all repos',
519
- 'ctrl+k ctrl+0 collapse all repos',
520
- 'ctrl+c quit mux',
521
- ];
522
- }
523
- if (normalized.includes('\n')) {
524
- return normalized
525
- .split('\n')
526
- .map((line) => line.trim())
527
- .filter((line) => line.length > 0);
528
- }
529
- return normalized
530
- .split(/\s{2,}/u)
531
- .map((segment) => segment.trim())
532
- .filter((segment) => segment.length > 0);
533
- }
534
-
535
- function shortcutRows(
536
- shortcutHint: string | undefined,
537
- shortcutsCollapsed: boolean,
538
- ): readonly WorkspaceRailViewRow[] {
539
- const rows: WorkspaceRailViewRow[] = [
540
- {
541
- kind: 'shortcut-header',
542
- text: `├─ shortcuts ${shortcutsCollapsed ? '[+]' : '[-]'}`,
543
- active: false,
544
- conversationSessionId: null,
545
- directoryKey: null,
546
- repositoryId: null,
547
- railAction: 'shortcuts.toggle',
548
- conversationStatus: null,
549
- },
550
- ];
551
- if (!shortcutsCollapsed) {
552
- const descriptions = shortcutDescriptionRows(shortcutHint);
553
- for (const description of descriptions) {
554
- rows.push({
555
- kind: 'shortcut-body',
556
- text: `│ ${description}`,
557
- active: false,
558
- conversationSessionId: null,
559
- directoryKey: null,
560
- repositoryId: null,
561
- railAction: null,
562
- conversationStatus: null,
563
- });
564
- }
565
- }
566
- return rows;
567
- }
568
-
569
738
  export function buildWorkspaceRailViewRows(
570
739
  model: WorkspaceRailModel,
571
740
  maxRows: number,
@@ -573,15 +742,8 @@ export function buildWorkspaceRailViewRows(
573
742
  const safeRows = Math.max(1, maxRows);
574
743
  const nowMs = model.nowMs ?? Date.now();
575
744
  const contentRows = buildContentRows(model, nowMs);
576
- const renderedShortcuts = shortcutRows(model.shortcutHint, model.shortcutsCollapsed ?? false);
577
-
578
- if (safeRows <= renderedShortcuts.length) {
579
- return renderedShortcuts.slice(renderedShortcuts.length - safeRows);
580
- }
581
-
582
- const contentCapacity = safeRows - renderedShortcuts.length;
583
- const rows: WorkspaceRailViewRow[] = [...contentRows.slice(0, Math.max(0, contentCapacity - 1))];
584
- while (rows.length < contentCapacity) {
745
+ const rows: WorkspaceRailViewRow[] = [...contentRows.slice(0, Math.max(0, safeRows - 1))];
746
+ while (rows.length < safeRows) {
585
747
  rows.push({
586
748
  kind: 'muted',
587
749
  text: '│',
@@ -603,12 +765,11 @@ export function buildWorkspaceRailViewRows(
603
765
  railAction: 'project.add',
604
766
  conversationStatus: null,
605
767
  };
606
- const projectActionRowIndex = Math.max(0, contentCapacity - 3);
768
+ const projectActionRowIndex = Math.max(0, safeRows - 3);
607
769
  rows.splice(projectActionRowIndex, 0, projectActionRow);
608
- if (rows.length > contentCapacity) {
609
- rows.length = contentCapacity;
770
+ if (rows.length > safeRows) {
771
+ rows.length = safeRows;
610
772
  }
611
- rows.push(...renderedShortcuts);
612
773
  return rows;
613
774
  }
614
775
 
@@ -644,27 +805,32 @@ export function actionAtWorkspaceRailCell(
644
805
  if (row === undefined) {
645
806
  return null;
646
807
  }
647
- if (row.railAction !== null) {
808
+ const normalizedCol = Math.max(0, Math.floor(colIndex));
809
+
810
+ if (row.kind === 'github-header') {
811
+ const collapsedGlyphCol = row.text.indexOf('▶');
812
+ const expandedGlyphCol = row.text.indexOf('▼');
813
+ const glyphCol = collapsedGlyphCol >= 0 ? collapsedGlyphCol : expandedGlyphCol;
814
+ if (glyphCol >= 0 && normalizedCol === glyphCol) {
815
+ return 'project.github.toggle';
816
+ }
648
817
  return row.railAction;
649
818
  }
650
- if (row.kind !== 'dir-header') {
651
- return null;
652
- }
653
- if (!row.text.includes(NEW_THREAD_INLINE_LABEL)) {
654
- return null;
655
- }
656
- const buttonStart =
657
- paneCols === null
658
- ? row.text.lastIndexOf(NEW_THREAD_INLINE_LABEL)
659
- : Math.max(0, Math.floor(paneCols) - NEW_THREAD_INLINE_LABEL.length);
660
- const normalizedCol = Math.max(0, Math.floor(colIndex));
661
- if (
662
- normalizedCol < buttonStart ||
663
- normalizedCol >= buttonStart + NEW_THREAD_INLINE_LABEL.length
664
- ) {
665
- return null;
819
+
820
+ if (row.kind === 'dir-header' && row.text.includes(NEW_THREAD_INLINE_LABEL)) {
821
+ const buttonStart =
822
+ paneCols === null
823
+ ? row.text.lastIndexOf(NEW_THREAD_INLINE_LABEL)
824
+ : Math.max(0, Math.floor(paneCols) - NEW_THREAD_INLINE_LABEL.length);
825
+ if (
826
+ normalizedCol >= buttonStart &&
827
+ normalizedCol < buttonStart + NEW_THREAD_INLINE_LABEL.length
828
+ ) {
829
+ return 'conversation.new';
830
+ }
666
831
  }
667
- return 'conversation.new';
832
+
833
+ return row.railAction;
668
834
  }
669
835
 
670
836
  export function projectIdAtWorkspaceRailRow(
@@ -1,20 +1,51 @@
1
1
  import {
2
- createUiSurface,
2
+ SurfaceBuffer,
3
3
  DEFAULT_UI_STYLE,
4
- drawUiText,
5
- fillUiRow,
6
- renderUiSurfaceAnsiRows,
7
4
  type UiStyle,
8
- } from '../ui/surface.ts';
9
- import { paintUiRow } from '../ui/kit.ts';
5
+ } from '../../packages/harness-ui/src/surface.ts';
6
+ import { UiKit } from '../../packages/harness-ui/src/kit.ts';
10
7
  import { measureDisplayWidth } from '../terminal/snapshot-oracle.ts';
11
8
  import { buildWorkspaceRailViewRows } from './workspace-rail-model.ts';
12
9
  import { getActiveMuxTheme, type MuxWorkspaceRailTheme } from '../ui/mux-theme.ts';
13
10
 
14
11
  type WorkspaceRailModel = Parameters<typeof buildWorkspaceRailViewRows>[0];
15
12
  type WorkspaceRailViewRow = ReturnType<typeof buildWorkspaceRailViewRows>[number];
13
+ type UiSurface = SurfaceBuffer;
16
14
 
17
15
  const INLINE_THREAD_BUTTON_LABEL = '[+ thread]';
16
+ const uiKit = new UiKit();
17
+
18
+ function createUiSurface(cols: number, rows: number, style: UiStyle): UiSurface {
19
+ return new SurfaceBuffer(cols, rows, style);
20
+ }
21
+
22
+ function drawUiText(
23
+ surface: UiSurface,
24
+ col: number,
25
+ row: number,
26
+ text: string,
27
+ style: UiStyle,
28
+ ): void {
29
+ surface.drawText(col, row, text, style);
30
+ }
31
+
32
+ function fillUiRow(surface: UiSurface, row: number, style: UiStyle): void {
33
+ surface.fillRow(row, style);
34
+ }
35
+
36
+ function paintUiRow(
37
+ surface: UiSurface,
38
+ row: number,
39
+ text: string,
40
+ textStyle: UiStyle,
41
+ fillStyle: UiStyle,
42
+ ): void {
43
+ uiKit.paintRow(surface, row, text, textStyle, fillStyle);
44
+ }
45
+
46
+ function renderUiSurfaceAnsiRows(surface: UiSurface): readonly string[] {
47
+ return surface.renderAnsiRows();
48
+ }
18
49
 
19
50
  function conversationStatusIconStyle(
20
51
  status: WorkspaceRailViewRow['conversationStatus'],
@@ -167,6 +198,14 @@ function paintWorkspaceRailRow(
167
198
  drawTreeRow(surface, rowIndex, row, theme, theme.metaStyle, theme.activeRowStyle);
168
199
  return;
169
200
  }
201
+ if (row.kind === 'github-header') {
202
+ drawTreeRow(surface, rowIndex, row, theme, theme.headerStyle, theme.activeRowStyle);
203
+ return;
204
+ }
205
+ if (row.kind === 'github-detail') {
206
+ drawTreeRow(surface, rowIndex, row, theme, theme.metaStyle, theme.activeRowStyle);
207
+ return;
208
+ }
170
209
  if (row.kind === 'conversation-title' || row.kind === 'conversation-body') {
171
210
  drawConversationRow(surface, rowIndex, row, theme);
172
211
  return;
@@ -191,22 +230,6 @@ function paintWorkspaceRailRow(
191
230
  paintUiRow(surface, rowIndex, row.text, theme.repositoryRowStyle, theme.normalStyle);
192
231
  return;
193
232
  }
194
- if (row.kind === 'shortcut-header') {
195
- const buttonLabel = row.text.endsWith('[+]') ? '[+]' : row.text.endsWith('[-]') ? '[-]' : null;
196
- if (buttonLabel === null) {
197
- drawTreeRow(surface, rowIndex, row, theme, theme.headerStyle, theme.activeRowStyle);
198
- } else {
199
- drawTreeRow(surface, rowIndex, row, theme, theme.headerStyle, theme.activeRowStyle, {
200
- buttonLabel,
201
- buttonStyle: theme.actionStyle,
202
- });
203
- }
204
- return;
205
- }
206
- if (row.kind === 'shortcut-body') {
207
- paintUiRow(surface, rowIndex, row.text, theme.shortcutStyle);
208
- return;
209
- }
210
233
  if (row.kind === 'action') {
211
234
  drawActionRow(surface, rowIndex, row, theme);
212
235
  return;
@@ -124,7 +124,7 @@ class SingleSessionBroker {
124
124
  for (const handlers of this.attachments.values()) {
125
125
  handlers.onData({
126
126
  cursor: entry.cursor,
127
- chunk: Buffer.from(fullChunk),
127
+ chunk: fullChunk,
128
128
  });
129
129
  }
130
130
  }
@@ -1,8 +1,8 @@
1
1
  import { writeFileSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import { createCanvas, type SKRSContext2D } from '@napi-rs/canvas';
4
- import type { TerminalSnapshotFrame } from '../src/terminal/snapshot-oracle.ts';
5
- import { readTerminalRecording } from '../src/recording/terminal-recording.ts';
4
+ import type { TerminalSnapshotFrame } from '../terminal/snapshot-oracle.ts';
5
+ import { readTerminalRecording } from './terminal-recording.ts';
6
6
 
7
7
  type GifEncPalette = number[][];
8
8
  type TerminalRecording = ReturnType<typeof readTerminalRecording>;