@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
@@ -0,0 +1,141 @@
1
+ import type { NimToolDefinition, NimToolPolicy } from '../../packages/nim-core/src/index.ts';
2
+
3
+ const RUNTIME_NIM_READ_TOOLS: readonly NimToolDefinition[] = [
4
+ {
5
+ name: 'directory.list',
6
+ description: 'List directories known to the current workspace.',
7
+ },
8
+ {
9
+ name: 'repository.list',
10
+ description: 'List repositories known to the current workspace.',
11
+ },
12
+ {
13
+ name: 'task.list',
14
+ description: 'List tasks known to the current workspace.',
15
+ },
16
+ {
17
+ name: 'session.list',
18
+ description: 'List active and historical sessions in the current workspace.',
19
+ },
20
+ ];
21
+
22
+ const RUNTIME_NIM_READ_POLICY: NimToolPolicy = {
23
+ hash: 'nim-control-plane-read-v1',
24
+ allow: RUNTIME_NIM_READ_TOOLS.map((tool) => tool.name),
25
+ deny: [],
26
+ };
27
+
28
+ export interface RuntimeNimToolBridgeOptions {
29
+ readonly listDirectories: () => Promise<readonly unknown[]>;
30
+ readonly listRepositories: () => Promise<readonly unknown[]>;
31
+ readonly listTasks: (limit: number) => Promise<readonly unknown[]>;
32
+ readonly listSessions: () => Promise<readonly unknown[]>;
33
+ readonly taskListLimit?: number;
34
+ }
35
+
36
+ export interface RuntimeNimToolBridgeInvokeInput {
37
+ readonly toolName: string;
38
+ readonly argumentsText?: string;
39
+ readonly argumentsValue?: unknown;
40
+ }
41
+
42
+ export interface RuntimeNimToolRuntime {
43
+ registerTools(tools: readonly NimToolDefinition[]): void;
44
+ setToolPolicy(policy: NimToolPolicy): void;
45
+ }
46
+
47
+ export class RuntimeNimToolBridge {
48
+ private readonly taskListLimit: number;
49
+
50
+ constructor(private readonly options: RuntimeNimToolBridgeOptions) {
51
+ this.taskListLimit = Math.max(1, options.taskListLimit ?? 100);
52
+ }
53
+
54
+ registerWithRuntime(runtime: RuntimeNimToolRuntime): void {
55
+ runtime.registerTools(RUNTIME_NIM_READ_TOOLS);
56
+ runtime.setToolPolicy(RUNTIME_NIM_READ_POLICY);
57
+ }
58
+
59
+ async invoke(input: RuntimeNimToolBridgeInvokeInput): Promise<unknown> {
60
+ if (input.toolName === 'directory.list') {
61
+ const directories = await this.options.listDirectories();
62
+ return {
63
+ count: directories.length,
64
+ directories,
65
+ };
66
+ }
67
+ if (input.toolName === 'repository.list') {
68
+ const repositories = await this.options.listRepositories();
69
+ return {
70
+ count: repositories.length,
71
+ repositories,
72
+ };
73
+ }
74
+ if (input.toolName === 'task.list') {
75
+ const limitInput: {
76
+ readonly argumentsText: string | undefined;
77
+ readonly argumentsValue: unknown;
78
+ readonly fallback: number;
79
+ } = {
80
+ argumentsText: input.argumentsText,
81
+ argumentsValue: input.argumentsValue,
82
+ fallback: this.taskListLimit,
83
+ };
84
+ const limit = resolveTaskListLimit(limitInput);
85
+ const tasks = await this.options.listTasks(limit);
86
+ return {
87
+ count: tasks.length,
88
+ limit,
89
+ tasks,
90
+ };
91
+ }
92
+ if (input.toolName === 'session.list') {
93
+ const sessions = await this.options.listSessions();
94
+ return {
95
+ count: sessions.length,
96
+ sessions,
97
+ };
98
+ }
99
+ throw new Error(`unsupported nim tool: ${input.toolName}`);
100
+ }
101
+ }
102
+
103
+ function resolvePositiveLimit(argumentsText: string, fallback: number): number {
104
+ const trimmed = argumentsText.trim();
105
+ if (trimmed.length === 0) {
106
+ return fallback;
107
+ }
108
+ const parsed = Number.parseInt(trimmed, 10);
109
+ if (!Number.isInteger(parsed) || parsed <= 0) {
110
+ throw new Error(`invalid task.list limit: ${trimmed}`);
111
+ }
112
+ return parsed;
113
+ }
114
+
115
+ function resolveTaskListLimit(input: {
116
+ readonly argumentsText: string | undefined;
117
+ readonly argumentsValue: unknown;
118
+ readonly fallback: number;
119
+ }): number {
120
+ const value = input.argumentsValue;
121
+ if (typeof value === 'number') {
122
+ if (!Number.isInteger(value) || value <= 0) {
123
+ throw new Error(`invalid task.list limit: ${String(value)}`);
124
+ }
125
+ return value;
126
+ }
127
+ if (typeof value === 'object' && value !== null && 'limit' in value) {
128
+ const limit = (value as { readonly limit?: unknown }).limit;
129
+ if (typeof limit === 'number') {
130
+ if (!Number.isInteger(limit) || limit <= 0) {
131
+ throw new Error(`invalid task.list limit: ${String(limit)}`);
132
+ }
133
+ return limit;
134
+ }
135
+ if (typeof limit === 'string') {
136
+ return resolvePositiveLimit(limit, input.fallback);
137
+ }
138
+ throw new Error(`invalid task.list limit: ${String(limit)}`);
139
+ }
140
+ return resolvePositiveLimit(input.argumentsText ?? '', input.fallback);
141
+ }
@@ -0,0 +1,45 @@
1
+ import type { StreamObservedEvent } from '../control-plane/stream-protocol.ts';
2
+ import {
3
+ applyObservedEventToHarnessSyncedStore,
4
+ type HarnessSyncedStore,
5
+ type HarnessSyncedStoreApplyResult,
6
+ } from '../core/store/harness-synced-store.ts';
7
+
8
+ export interface RuntimeObservedEventProjectionInput {
9
+ readonly subscriptionId: string;
10
+ readonly cursor: number;
11
+ readonly event: StreamObservedEvent;
12
+ }
13
+
14
+ export interface RuntimeObservedEventProjectionResult {
15
+ readonly cursorAccepted: boolean;
16
+ readonly previousCursor: number | null;
17
+ }
18
+
19
+ export interface RuntimeObservedEventProjectionPipelineOptions {
20
+ readonly syncedStore: HarnessSyncedStore;
21
+ readonly applyWorkspaceProjection: (reduction: HarnessSyncedStoreApplyResult) => void;
22
+ readonly applyTaskPlanningProjection: (reduction: HarnessSyncedStoreApplyResult) => void;
23
+ readonly applyDirectoryGitProjection: (event: StreamObservedEvent) => void;
24
+ }
25
+
26
+ export function applyRuntimeObservedEventProjection(
27
+ input: RuntimeObservedEventProjectionInput,
28
+ options: RuntimeObservedEventProjectionPipelineOptions,
29
+ ): RuntimeObservedEventProjectionResult {
30
+ const reduced = applyObservedEventToHarnessSyncedStore(options.syncedStore, input);
31
+ if (!reduced.cursorAccepted) {
32
+ return {
33
+ cursorAccepted: false,
34
+ previousCursor: reduced.previousCursor,
35
+ };
36
+ }
37
+ options.applyWorkspaceProjection(reduced);
38
+ // Directory git status updates remain an explicit non-synced projection boundary.
39
+ options.applyDirectoryGitProjection(input.event);
40
+ options.applyTaskPlanningProjection(reduced);
41
+ return {
42
+ cursorAccepted: true,
43
+ previousCursor: reduced.previousCursor,
44
+ };
45
+ }
@@ -10,7 +10,7 @@ interface RuntimeProcessTarget {
10
10
  off(event: 'unhandledRejection', listener: (reason: unknown) => void): RuntimeProcessTarget;
11
11
  }
12
12
 
13
- interface RuntimeProcessWiringOptions {
13
+ export interface RuntimeProcessWiringOptions {
14
14
  readonly onInput: (chunk: Buffer) => void;
15
15
  readonly onResize: () => void;
16
16
  readonly requestStop: () => void;
@@ -18,52 +18,45 @@ interface RuntimeProcessWiringOptions {
18
18
  readonly target?: RuntimeProcessTarget;
19
19
  }
20
20
 
21
- export class RuntimeProcessWiring {
22
- private readonly target: RuntimeProcessTarget;
23
-
24
- constructor(private readonly options: RuntimeProcessWiringOptions) {
25
- this.target = options.target ?? process;
26
- }
27
-
28
- attach(): void {
29
- this.target.stdin.on('data', this.onInputSafe);
30
- this.target.stdout.on('resize', this.onResizeSafe);
31
- this.target.on('SIGINT', this.options.requestStop);
32
- this.target.on('SIGTERM', this.options.requestStop);
33
- this.target.once('uncaughtException', this.onUncaughtException);
34
- this.target.once('unhandledRejection', this.onUnhandledRejection);
35
- }
36
-
37
- detach(): void {
38
- this.target.stdin.off('data', this.onInputSafe);
39
- this.target.stdout.off('resize', this.onResizeSafe);
40
- this.target.off('SIGINT', this.options.requestStop);
41
- this.target.off('SIGTERM', this.options.requestStop);
42
- this.target.off('uncaughtException', this.onUncaughtException);
43
- this.target.off('unhandledRejection', this.onUnhandledRejection);
44
- }
45
-
46
- private readonly onInputSafe = (chunk: Buffer): void => {
21
+ export function attachRuntimeProcessWiring(options: RuntimeProcessWiringOptions): () => void {
22
+ const target = options.target ?? process;
23
+ const onInputSafe = (chunk: Buffer): void => {
47
24
  try {
48
- this.options.onInput(chunk);
25
+ options.onInput(chunk);
49
26
  } catch (error: unknown) {
50
- this.options.handleRuntimeFatal('stdin-data', error);
27
+ options.handleRuntimeFatal('stdin-data', error);
51
28
  }
52
29
  };
53
30
 
54
- private readonly onResizeSafe = (): void => {
31
+ const onResizeSafe = (): void => {
55
32
  try {
56
- this.options.onResize();
33
+ options.onResize();
57
34
  } catch (error: unknown) {
58
- this.options.handleRuntimeFatal('stdout-resize', error);
35
+ options.handleRuntimeFatal('stdout-resize', error);
59
36
  }
60
37
  };
61
38
 
62
- private readonly onUncaughtException = (error: Error): void => {
63
- this.options.handleRuntimeFatal('uncaught-exception', error);
39
+ const onUncaughtException = (error: Error): void => {
40
+ options.handleRuntimeFatal('uncaught-exception', error);
41
+ };
42
+
43
+ const onUnhandledRejection = (reason: unknown): void => {
44
+ options.handleRuntimeFatal('unhandled-rejection', reason);
64
45
  };
65
46
 
66
- private readonly onUnhandledRejection = (reason: unknown): void => {
67
- this.options.handleRuntimeFatal('unhandled-rejection', reason);
47
+ target.stdin.on('data', onInputSafe);
48
+ target.stdout.on('resize', onResizeSafe);
49
+ target.on('SIGINT', options.requestStop);
50
+ target.on('SIGTERM', options.requestStop);
51
+ target.once('uncaughtException', onUncaughtException);
52
+ target.once('unhandledRejection', onUnhandledRejection);
53
+
54
+ return (): void => {
55
+ target.stdin.off('data', onInputSafe);
56
+ target.stdout.off('resize', onResizeSafe);
57
+ target.off('SIGINT', options.requestStop);
58
+ target.off('SIGTERM', options.requestStop);
59
+ target.off('uncaughtException', onUncaughtException);
60
+ target.off('unhandledRejection', onUnhandledRejection);
68
61
  };
69
62
  }
@@ -0,0 +1,164 @@
1
+ import type { ProjectPaneGitHubReviewSummary } from '../mux/project-pane-github-review.ts';
2
+
3
+ interface QueueLatestControlPlaneOp {
4
+ (
5
+ key: string,
6
+ task: (options: { readonly signal: AbortSignal }) => Promise<void>,
7
+ label: string,
8
+ ): void;
9
+ }
10
+
11
+ interface RuntimeProjectPaneGitHubReviewCacheOptions {
12
+ readonly ttlMs: number;
13
+ readonly refreshIntervalMs: number;
14
+ readonly queueLatestControlPlaneOp: QueueLatestControlPlaneOp;
15
+ readonly loadReview: (
16
+ directoryId: string,
17
+ options: RuntimeProjectPaneGitHubReviewCacheRequestOptions,
18
+ ) => Promise<ProjectPaneGitHubReviewSummary>;
19
+ readonly onUpdate: (directoryId: string, review: ProjectPaneGitHubReviewSummary) => void;
20
+ readonly formatErrorMessage: (error: unknown) => string;
21
+ readonly nowMs?: () => number;
22
+ readonly setInterval?: (callback: () => void, ms: number) => NodeJS.Timeout;
23
+ readonly clearInterval?: (timer: NodeJS.Timeout) => void;
24
+ }
25
+
26
+ interface RuntimeProjectPaneGitHubReviewCacheRequestOptions {
27
+ readonly forceRefresh?: boolean;
28
+ }
29
+
30
+ interface CacheEntry {
31
+ review: ProjectPaneGitHubReviewSummary | null;
32
+ fetchedAtMs: number | null;
33
+ inFlight: boolean;
34
+ }
35
+
36
+ function loadingState(
37
+ previous: ProjectPaneGitHubReviewSummary | null,
38
+ ): ProjectPaneGitHubReviewSummary {
39
+ return {
40
+ status: 'loading',
41
+ branchName: previous?.branchName ?? null,
42
+ branchSource: previous?.branchSource ?? null,
43
+ pr: previous?.pr ?? null,
44
+ openThreads: previous?.openThreads ?? [],
45
+ resolvedThreads: previous?.resolvedThreads ?? [],
46
+ errorMessage: null,
47
+ };
48
+ }
49
+
50
+ function errorState(
51
+ previous: ProjectPaneGitHubReviewSummary | null,
52
+ message: string,
53
+ ): ProjectPaneGitHubReviewSummary {
54
+ return {
55
+ status: 'error',
56
+ branchName: previous?.branchName ?? null,
57
+ branchSource: previous?.branchSource ?? null,
58
+ pr: previous?.pr ?? null,
59
+ openThreads: previous?.openThreads ?? [],
60
+ resolvedThreads: previous?.resolvedThreads ?? [],
61
+ errorMessage: message,
62
+ };
63
+ }
64
+
65
+ export class RuntimeProjectPaneGitHubReviewCacheEngine {
66
+ private readonly entries = new Map<string, CacheEntry>();
67
+ private readonly nowMs: () => number;
68
+ private readonly setIntervalFn: (callback: () => void, ms: number) => NodeJS.Timeout;
69
+ private readonly clearIntervalFn: (timer: NodeJS.Timeout) => void;
70
+ private refreshTimer: NodeJS.Timeout | null = null;
71
+
72
+ constructor(private readonly options: RuntimeProjectPaneGitHubReviewCacheOptions) {
73
+ this.nowMs = options.nowMs ?? Date.now;
74
+ this.setIntervalFn = options.setInterval ?? setInterval;
75
+ this.clearIntervalFn = options.clearInterval ?? clearInterval;
76
+ }
77
+
78
+ request(
79
+ directoryId: string,
80
+ requestOptions: RuntimeProjectPaneGitHubReviewCacheRequestOptions = {},
81
+ ): void {
82
+ const entry = this.entries.get(directoryId) ?? {
83
+ review: null,
84
+ fetchedAtMs: null,
85
+ inFlight: false,
86
+ };
87
+ this.entries.set(directoryId, entry);
88
+ const forceRefresh = requestOptions.forceRefresh === true;
89
+ if (entry.inFlight) {
90
+ return;
91
+ }
92
+ if (!forceRefresh && this.isFresh(entry)) {
93
+ return;
94
+ }
95
+ const previous = entry.review;
96
+ entry.inFlight = true;
97
+ const nextLoading = loadingState(previous);
98
+ entry.review = nextLoading;
99
+ this.options.onUpdate(directoryId, nextLoading);
100
+
101
+ this.options.queueLatestControlPlaneOp(
102
+ `project-pane-github-review:${directoryId}`,
103
+ async ({ signal }) => {
104
+ if (signal.aborted) {
105
+ return;
106
+ }
107
+ try {
108
+ const loaded = await this.options.loadReview(directoryId, {
109
+ forceRefresh,
110
+ });
111
+ if (signal.aborted) {
112
+ return;
113
+ }
114
+ entry.review = loaded;
115
+ entry.fetchedAtMs = this.nowMs();
116
+ this.options.onUpdate(directoryId, loaded);
117
+ } catch (error: unknown) {
118
+ if (signal.aborted) {
119
+ return;
120
+ }
121
+ const message = this.options.formatErrorMessage(error);
122
+ const nextError = errorState(previous, message);
123
+ entry.review = nextError;
124
+ this.options.onUpdate(directoryId, nextError);
125
+ } finally {
126
+ entry.inFlight = false;
127
+ }
128
+ },
129
+ 'project-pane-github-review',
130
+ );
131
+ }
132
+
133
+ startAutoRefresh(resolveDirectoryId: () => string | null): void {
134
+ this.stopAutoRefresh();
135
+ if (this.options.refreshIntervalMs <= 0) {
136
+ return;
137
+ }
138
+ this.refreshTimer = this.setIntervalFn(() => {
139
+ const directoryId = resolveDirectoryId();
140
+ if (directoryId === null) {
141
+ return;
142
+ }
143
+ this.request(directoryId);
144
+ }, this.options.refreshIntervalMs);
145
+ }
146
+
147
+ stopAutoRefresh(): void {
148
+ if (this.refreshTimer === null) {
149
+ return;
150
+ }
151
+ this.clearIntervalFn(this.refreshTimer);
152
+ this.refreshTimer = null;
153
+ }
154
+
155
+ private isFresh(entry: CacheEntry): boolean {
156
+ if (entry.review?.status !== 'ready') {
157
+ return false;
158
+ }
159
+ if (entry.fetchedAtMs === null) {
160
+ return false;
161
+ }
162
+ return this.nowMs() - entry.fetchedAtMs < this.options.ttlMs;
163
+ }
164
+ }
@@ -1,10 +1,10 @@
1
- interface RuntimeRenderFlushResult {
1
+ export interface RuntimeRenderFlushResult {
2
2
  readonly changedRowCount: number;
3
3
  readonly wroteOutput: boolean;
4
4
  readonly shouldShowCursor: boolean;
5
5
  }
6
6
 
7
- interface RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout> {
7
+ export interface RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout> {
8
8
  readonly layout: TLayout;
9
9
  readonly projectPaneActive: boolean;
10
10
  readonly homePaneActive: boolean;
@@ -16,7 +16,7 @@ interface RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout> {
16
16
  readonly rightRows: readonly string[];
17
17
  }
18
18
 
19
- interface RuntimeRenderFlushOptions<
19
+ export interface RuntimeRenderFlushOptions<
20
20
  TConversation,
21
21
  TFrame,
22
22
  TSelection,
@@ -66,81 +66,74 @@ interface RuntimeRenderFlushOptions<
66
66
  readonly recordRenderSample: (durationMs: number, changedRowCount: number) => void;
67
67
  }
68
68
 
69
- export class RuntimeRenderFlush<
69
+ export function flushRuntimeRender<
70
70
  TConversation,
71
71
  TFrame,
72
72
  TSelection,
73
73
  TLayout,
74
74
  TModalOverlay,
75
75
  TStatusRow,
76
- > {
77
- constructor(
78
- private readonly options: RuntimeRenderFlushOptions<
79
- TConversation,
80
- TFrame,
81
- TSelection,
82
- TLayout,
83
- TModalOverlay,
84
- TStatusRow
85
- >,
86
- ) {}
87
-
88
- flushRender(input: RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout>): void {
89
- const renderStartedAtNs = this.options.perfNowNs();
90
- const baseStatusFooter =
91
- !input.projectPaneActive && !input.homePaneActive && input.activeConversation !== null
92
- ? this.options.statusFooterForConversation(input.activeConversation)
93
- : '';
94
- const statusNotice = this.options.currentStatusNotice();
95
- const statusFooter =
96
- statusNotice === null || statusNotice.length === 0
97
- ? baseStatusFooter
98
- : `${baseStatusFooter.length > 0 ? `${baseStatusFooter} ` : ''}${statusNotice}`;
99
- const statusRow = this.options.currentStatusRow();
100
- this.options.onStatusLineComposed?.({
76
+ >(
77
+ options: RuntimeRenderFlushOptions<
78
+ TConversation,
79
+ TFrame,
80
+ TSelection,
81
+ TLayout,
82
+ TModalOverlay,
83
+ TStatusRow
84
+ >,
85
+ input: RuntimeRenderFlushInput<TConversation, TFrame, TSelection, TLayout>,
86
+ ): void {
87
+ const renderStartedAtNs = options.perfNowNs();
88
+ const baseStatusFooter =
89
+ !input.projectPaneActive && !input.homePaneActive && input.activeConversation !== null
90
+ ? options.statusFooterForConversation(input.activeConversation)
91
+ : '';
92
+ const statusNotice = options.currentStatusNotice();
93
+ const statusFooter =
94
+ statusNotice === null || statusNotice.length === 0
95
+ ? baseStatusFooter
96
+ : `${baseStatusFooter.length > 0 ? `${baseStatusFooter} ` : ''}${statusNotice}`;
97
+ const statusRow = options.currentStatusRow();
98
+ options.onStatusLineComposed?.({
99
+ activeConversation: input.activeConversation,
100
+ statusFooter,
101
+ statusRow,
102
+ projectPaneActive: input.projectPaneActive,
103
+ homePaneActive: input.homePaneActive,
104
+ });
105
+ const rows = options.buildRenderRows(
106
+ input.layout,
107
+ input.railAnsiRows,
108
+ input.rightRows,
109
+ statusRow,
110
+ statusFooter,
111
+ );
112
+ const modalOverlay = options.buildModalOverlay();
113
+ if (modalOverlay !== null) {
114
+ options.applyModalOverlay(rows, modalOverlay);
115
+ }
116
+ const selectionOverlay =
117
+ input.rightFrame === null
118
+ ? ''
119
+ : options.renderSelectionOverlay(input.layout, input.rightFrame, input.renderSelection);
120
+ const flushResult = options.flush({
121
+ layout: input.layout,
122
+ rows,
123
+ rightFrame: input.rightFrame,
124
+ selectionRows: input.selectionRows,
125
+ selectionOverlay,
126
+ });
127
+ const changedRowCount = flushResult.changedRowCount;
128
+ if (flushResult.wroteOutput) {
129
+ options.onFlushOutput({
101
130
  activeConversation: input.activeConversation,
102
- statusFooter,
103
- statusRow,
104
- projectPaneActive: input.projectPaneActive,
105
- homePaneActive: input.homePaneActive,
106
- });
107
- const rows = this.options.buildRenderRows(
108
- input.layout,
109
- input.railAnsiRows,
110
- input.rightRows,
111
- statusRow,
112
- statusFooter,
113
- );
114
- const modalOverlay = this.options.buildModalOverlay();
115
- if (modalOverlay !== null) {
116
- this.options.applyModalOverlay(rows, modalOverlay);
117
- }
118
- const selectionOverlay =
119
- input.rightFrame === null
120
- ? ''
121
- : this.options.renderSelectionOverlay(
122
- input.layout,
123
- input.rightFrame,
124
- input.renderSelection,
125
- );
126
- const flushResult = this.options.flush({
127
- layout: input.layout,
128
- rows,
129
131
  rightFrame: input.rightFrame,
130
- selectionRows: input.selectionRows,
131
- selectionOverlay,
132
+ rows,
133
+ flushResult,
134
+ changedRowCount,
132
135
  });
133
- const changedRowCount = flushResult.changedRowCount;
134
- if (flushResult.wroteOutput) {
135
- this.options.onFlushOutput({
136
- activeConversation: input.activeConversation,
137
- rightFrame: input.rightFrame,
138
- rows,
139
- flushResult,
140
- changedRowCount,
141
- });
142
- }
143
- const renderDurationMs = Number(this.options.perfNowNs() - renderStartedAtNs) / 1e6;
144
- this.options.recordRenderSample(renderDurationMs, changedRowCount);
145
136
  }
137
+ const renderDurationMs = Number(options.perfNowNs() - renderStartedAtNs) / 1e6;
138
+ options.recordRenderSample(renderDurationMs, changedRowCount);
146
139
  }