@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
@@ -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
  }