@jmoyers/harness 0.1.0

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
@@ -0,0 +1,156 @@
1
+ import {
2
+ reduceTaskEditorPromptInput,
3
+ type TaskEditorPromptInputState,
4
+ } from './modal-input-reducers.ts';
5
+
6
+ interface TaskEditorPromptState extends TaskEditorPromptInputState {
7
+ mode: 'create' | 'edit';
8
+ taskId: string | null;
9
+ error: string | null;
10
+ }
11
+
12
+ interface TaskEditorSubmitPayload {
13
+ mode: 'create' | 'edit';
14
+ taskId: string | null;
15
+ repositoryId: string;
16
+ title: string;
17
+ description: string;
18
+ commandLabel: string;
19
+ }
20
+
21
+ interface HandleTaskEditorPromptInputResult {
22
+ handled: boolean;
23
+ nextPrompt?: TaskEditorPromptState | null;
24
+ markDirty: boolean;
25
+ submitPayload?: TaskEditorSubmitPayload;
26
+ }
27
+
28
+ interface HandleTaskEditorPromptInputOptions {
29
+ input: Buffer;
30
+ prompt: TaskEditorPromptState | null;
31
+ isQuitShortcut: (input: Buffer) => boolean;
32
+ dismissOnOutsideClick: (input: Buffer, dismiss: () => void) => boolean;
33
+ }
34
+
35
+ export function handleTaskEditorPromptInput(
36
+ options: HandleTaskEditorPromptInputOptions,
37
+ ): HandleTaskEditorPromptInputResult {
38
+ const { input, prompt, isQuitShortcut, dismissOnOutsideClick } = options;
39
+ if (prompt === null) {
40
+ return {
41
+ handled: false,
42
+ markDirty: false,
43
+ };
44
+ }
45
+ if (input.length === 1 && input[0] === 0x03) {
46
+ return {
47
+ handled: false,
48
+ markDirty: false,
49
+ };
50
+ }
51
+ if (isQuitShortcut(input)) {
52
+ return {
53
+ handled: true,
54
+ nextPrompt: null,
55
+ markDirty: true,
56
+ };
57
+ }
58
+
59
+ let dismissed = false;
60
+ if (
61
+ dismissOnOutsideClick(input, () => {
62
+ dismissed = true;
63
+ })
64
+ ) {
65
+ return {
66
+ handled: true,
67
+ ...(dismissed
68
+ ? {
69
+ nextPrompt: null,
70
+ markDirty: true,
71
+ }
72
+ : {
73
+ markDirty: false,
74
+ }),
75
+ };
76
+ }
77
+
78
+ const reduced = reduceTaskEditorPromptInput(prompt, input);
79
+ const nextTitle = reduced.title;
80
+ const nextDescription = reduced.description;
81
+ const nextFieldIndex = reduced.fieldIndex;
82
+ const nextRepositoryIndex = reduced.repositoryIndex;
83
+ const submit = reduced.submit;
84
+ const changed =
85
+ nextTitle !== prompt.title ||
86
+ nextDescription !== prompt.description ||
87
+ nextFieldIndex !== prompt.fieldIndex ||
88
+ nextRepositoryIndex !== prompt.repositoryIndex;
89
+
90
+ const changedPrompt = changed
91
+ ? {
92
+ ...prompt,
93
+ title: nextTitle,
94
+ description: nextDescription,
95
+ fieldIndex: nextFieldIndex,
96
+ repositoryIndex: nextRepositoryIndex,
97
+ error: null,
98
+ }
99
+ : prompt;
100
+
101
+ if (!submit) {
102
+ return {
103
+ handled: true,
104
+ ...(changed
105
+ ? {
106
+ nextPrompt: changedPrompt,
107
+ markDirty: true,
108
+ }
109
+ : {
110
+ markDirty: false,
111
+ }),
112
+ };
113
+ }
114
+
115
+ const repositoryId = prompt.repositoryIds[nextRepositoryIndex] ?? null;
116
+ const title = nextTitle.trim();
117
+ if (title.length === 0) {
118
+ return {
119
+ handled: true,
120
+ nextPrompt: {
121
+ ...changedPrompt,
122
+ error: 'title required',
123
+ },
124
+ markDirty: true,
125
+ };
126
+ }
127
+ if (repositoryId === null) {
128
+ return {
129
+ handled: true,
130
+ nextPrompt: {
131
+ ...changedPrompt,
132
+ error: 'repository required',
133
+ },
134
+ markDirty: true,
135
+ };
136
+ }
137
+ return {
138
+ handled: true,
139
+ ...(changed
140
+ ? {
141
+ nextPrompt: changedPrompt,
142
+ markDirty: true,
143
+ }
144
+ : {
145
+ markDirty: false,
146
+ }),
147
+ submitPayload: {
148
+ mode: prompt.mode,
149
+ taskId: prompt.taskId,
150
+ repositoryId,
151
+ title,
152
+ description: nextDescription,
153
+ commandLabel: prompt.mode === 'create' ? 'tasks-create' : 'tasks-edit',
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,87 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { StreamCommand } from '../../control-plane/stream-protocol.ts';
3
+
4
+ interface ControlPlaneCommandClient {
5
+ sendCommand(command: StreamCommand): Promise<Record<string, unknown>>;
6
+ }
7
+
8
+ interface StreamScope {
9
+ readonly tenantId: string;
10
+ readonly userId: string;
11
+ readonly workspaceId: string;
12
+ }
13
+
14
+ export async function readObservedStreamCursorBaseline(
15
+ streamClient: ControlPlaneCommandClient,
16
+ scope: StreamScope,
17
+ ): Promise<number | null> {
18
+ const subscribed = await streamClient.sendCommand({
19
+ type: 'stream.subscribe',
20
+ tenantId: scope.tenantId,
21
+ userId: scope.userId,
22
+ workspaceId: scope.workspaceId,
23
+ conversationId: `cursor-probe-${randomUUID()}`,
24
+ });
25
+ const subscriptionId = subscribed['subscriptionId'];
26
+ if (typeof subscriptionId !== 'string' || subscriptionId.length === 0) {
27
+ throw new Error('control-plane stream.subscribe returned malformed subscription id');
28
+ }
29
+ try {
30
+ const cursor = subscribed['cursor'];
31
+ if (typeof cursor !== 'number' || !Number.isInteger(cursor) || cursor < 0) {
32
+ return null;
33
+ }
34
+ return cursor;
35
+ } finally {
36
+ try {
37
+ await streamClient.sendCommand({
38
+ type: 'stream.unsubscribe',
39
+ subscriptionId,
40
+ });
41
+ } catch {
42
+ // Best-effort unsubscribe only.
43
+ }
44
+ }
45
+ }
46
+
47
+ export async function subscribeObservedStream(
48
+ streamClient: ControlPlaneCommandClient,
49
+ scope: StreamScope,
50
+ afterCursor: number | null,
51
+ ): Promise<string> {
52
+ const command: {
53
+ type: 'stream.subscribe';
54
+ tenantId: string;
55
+ userId: string;
56
+ workspaceId: string;
57
+ afterCursor?: number;
58
+ } = {
59
+ type: 'stream.subscribe',
60
+ tenantId: scope.tenantId,
61
+ userId: scope.userId,
62
+ workspaceId: scope.workspaceId,
63
+ };
64
+ if (afterCursor !== null) {
65
+ command.afterCursor = afterCursor;
66
+ }
67
+ const subscribed = await streamClient.sendCommand(command);
68
+ const subscriptionId = subscribed['subscriptionId'];
69
+ if (typeof subscriptionId !== 'string') {
70
+ throw new Error('control-plane stream.subscribe returned malformed subscription id');
71
+ }
72
+ return subscriptionId;
73
+ }
74
+
75
+ export async function unsubscribeObservedStream(
76
+ streamClient: ControlPlaneCommandClient,
77
+ subscriptionId: string,
78
+ ): Promise<void> {
79
+ try {
80
+ await streamClient.sendCommand({
81
+ type: 'stream.unsubscribe',
82
+ subscriptionId,
83
+ });
84
+ } catch {
85
+ // Best-effort unsubscribe only.
86
+ }
87
+ }
@@ -0,0 +1,128 @@
1
+ export function parseOscRgbHex(value: string): string | null {
2
+ if (!value.startsWith('rgb:')) {
3
+ return null;
4
+ }
5
+
6
+ const components = value.slice(4).split('/');
7
+ if (components.length !== 3) {
8
+ return null;
9
+ }
10
+
11
+ const bytes: string[] = [];
12
+ for (const component of components) {
13
+ const normalized = component.trim();
14
+ if (normalized.length < 1 || normalized.length > 4) {
15
+ return null;
16
+ }
17
+ if (!/^[0-9a-fA-F]+$/.test(normalized)) {
18
+ return null;
19
+ }
20
+
21
+ const raw = Number.parseInt(normalized, 16);
22
+ const max = (1 << (normalized.length * 4)) - 1;
23
+ const scaled = Math.round((raw * 255) / max);
24
+ bytes.push(scaled.toString(16).padStart(2, '0'));
25
+ }
26
+
27
+ return `${bytes[0]}${bytes[1]}${bytes[2]}`;
28
+ }
29
+
30
+ export function extractOscColorReplies(buffer: string): {
31
+ readonly remainder: string;
32
+ readonly foregroundHex?: string;
33
+ readonly backgroundHex?: string;
34
+ readonly indexedHexByCode: Record<number, string>;
35
+ } {
36
+ let remainder = buffer;
37
+ let foregroundHex: string | undefined;
38
+ let backgroundHex: string | undefined;
39
+ const indexedHexByCode: Record<number, string> = {};
40
+
41
+ while (true) {
42
+ const start = remainder.indexOf('\u001b]');
43
+ if (start < 0) {
44
+ break;
45
+ }
46
+ if (start > 0) {
47
+ remainder = remainder.slice(start);
48
+ }
49
+
50
+ const bellTerminator = remainder.indexOf('\u0007', 2);
51
+ const stTerminator = remainder.indexOf('\u001b\\', 2);
52
+ let end = -1;
53
+ let terminatorLength = 0;
54
+
55
+ if (bellTerminator >= 0 && (stTerminator < 0 || bellTerminator < stTerminator)) {
56
+ end = bellTerminator;
57
+ terminatorLength = 1;
58
+ } else if (stTerminator >= 0) {
59
+ end = stTerminator;
60
+ terminatorLength = 2;
61
+ }
62
+
63
+ if (end < 0) {
64
+ break;
65
+ }
66
+
67
+ const payload = remainder.slice(2, end);
68
+ remainder = remainder.slice(end + terminatorLength);
69
+ const separator = payload.indexOf(';');
70
+ if (separator < 0) {
71
+ continue;
72
+ }
73
+
74
+ const code = payload.slice(0, separator);
75
+ if (code === '10') {
76
+ const hex = parseOscRgbHex(payload.slice(separator + 1));
77
+ if (hex !== null) {
78
+ foregroundHex = hex;
79
+ }
80
+ continue;
81
+ }
82
+
83
+ if (code === '11') {
84
+ const hex = parseOscRgbHex(payload.slice(separator + 1));
85
+ if (hex !== null) {
86
+ backgroundHex = hex;
87
+ }
88
+ continue;
89
+ }
90
+
91
+ if (code === '4') {
92
+ const value = payload.slice(separator + 1);
93
+ const paletteSeparator = value.indexOf(';');
94
+ if (paletteSeparator < 0) {
95
+ continue;
96
+ }
97
+ const paletteIndexRaw = value.slice(0, paletteSeparator).trim();
98
+ const paletteValueRaw = value.slice(paletteSeparator + 1);
99
+ const parsedIndex = Number.parseInt(paletteIndexRaw, 10);
100
+ if (!Number.isInteger(parsedIndex) || parsedIndex < 0 || parsedIndex > 255) {
101
+ continue;
102
+ }
103
+ const hex = parseOscRgbHex(paletteValueRaw);
104
+ if (hex !== null) {
105
+ indexedHexByCode[parsedIndex] = hex;
106
+ }
107
+ }
108
+ }
109
+
110
+ if (remainder.length > 512) {
111
+ remainder = remainder.slice(-512);
112
+ }
113
+
114
+ return {
115
+ remainder,
116
+ ...(foregroundHex !== undefined
117
+ ? {
118
+ foregroundHex,
119
+ }
120
+ : {}),
121
+ ...(backgroundHex !== undefined
122
+ ? {
123
+ backgroundHex,
124
+ }
125
+ : {}),
126
+ indexedHexByCode,
127
+ };
128
+ }
@@ -0,0 +1,108 @@
1
+ interface HandlePaneDividerDragInputOptions {
2
+ paneDividerDragActive: boolean;
3
+ isMouseRelease: boolean;
4
+ isWheelMouseCode: boolean;
5
+ mouseCol: number;
6
+ setPaneDividerDragActive: (active: boolean) => void;
7
+ applyPaneDividerAtCol: (col: number) => void;
8
+ markDirty: () => void;
9
+ }
10
+
11
+ export function handlePaneDividerDragInput(options: HandlePaneDividerDragInputOptions): boolean {
12
+ if (!options.paneDividerDragActive) {
13
+ return false;
14
+ }
15
+ if (options.isMouseRelease) {
16
+ options.setPaneDividerDragActive(false);
17
+ options.markDirty();
18
+ return true;
19
+ }
20
+ if (!options.isWheelMouseCode) {
21
+ options.applyPaneDividerAtCol(options.mouseCol);
22
+ return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ interface HandleSeparatorPointerPressOptions {
28
+ target: string;
29
+ isLeftButtonPress: boolean;
30
+ hasAltModifier: boolean;
31
+ mouseCol: number;
32
+ setPaneDividerDragActive: (active: boolean) => void;
33
+ applyPaneDividerAtCol: (col: number) => void;
34
+ }
35
+
36
+ export function handleSeparatorPointerPress(options: HandleSeparatorPointerPressOptions): boolean {
37
+ if (options.target !== 'separator' || !options.isLeftButtonPress || options.hasAltModifier) {
38
+ return false;
39
+ }
40
+ options.setPaneDividerDragActive(true);
41
+ options.applyPaneDividerAtCol(options.mouseCol);
42
+ return true;
43
+ }
44
+
45
+ interface HandleMainPaneWheelInputOptions {
46
+ target: string;
47
+ wheelDelta: number | null;
48
+ mainPaneMode: 'conversation' | 'project' | 'home';
49
+ onProjectWheel: (delta: number) => void;
50
+ onHomeWheel: (delta: number) => void;
51
+ onConversationWheel: (delta: number) => void;
52
+ markDirty: () => void;
53
+ }
54
+
55
+ export function handleMainPaneWheelInput(options: HandleMainPaneWheelInputOptions): boolean {
56
+ if (options.wheelDelta === null || options.target !== 'right') {
57
+ return false;
58
+ }
59
+ if (options.mainPaneMode === 'project') {
60
+ options.onProjectWheel(options.wheelDelta);
61
+ } else if (options.mainPaneMode === 'home') {
62
+ options.onHomeWheel(options.wheelDelta);
63
+ } else {
64
+ options.onConversationWheel(options.wheelDelta);
65
+ }
66
+ options.markDirty();
67
+ return true;
68
+ }
69
+
70
+ interface HomePaneDragState {
71
+ readonly kind: 'task' | 'repository';
72
+ readonly itemId: string;
73
+ readonly startedRowIndex: number;
74
+ readonly latestRowIndex: number;
75
+ readonly hasDragged: boolean;
76
+ }
77
+
78
+ interface HandleHomePaneDragMoveOptions {
79
+ homePaneDragState: HomePaneDragState | null;
80
+ mainPaneMode: 'conversation' | 'project' | 'home';
81
+ target: string;
82
+ isSelectionDrag: boolean;
83
+ hasAltModifier: boolean;
84
+ rowIndex: number;
85
+ setHomePaneDragState: (next: HomePaneDragState) => void;
86
+ markDirty: () => void;
87
+ }
88
+
89
+ export function handleHomePaneDragMove(options: HandleHomePaneDragMoveOptions): boolean {
90
+ if (
91
+ options.homePaneDragState === null ||
92
+ options.mainPaneMode !== 'home' ||
93
+ options.target !== 'right' ||
94
+ !options.isSelectionDrag ||
95
+ options.hasAltModifier
96
+ ) {
97
+ return false;
98
+ }
99
+ options.setHomePaneDragState({
100
+ ...options.homePaneDragState,
101
+ latestRowIndex: options.rowIndex,
102
+ hasDragged:
103
+ options.homePaneDragState.hasDragged ||
104
+ options.rowIndex !== options.homePaneDragState.startedRowIndex,
105
+ });
106
+ options.markDirty();
107
+ return true;
108
+ }
@@ -0,0 +1,53 @@
1
+ interface RefreshProcessUsageSnapshotsOptions<TConversation, TSample> {
2
+ conversations: ReadonlyMap<string, TConversation>;
3
+ processUsageBySessionId: Map<string, TSample>;
4
+ readProcessUsageSample: (processId: number | null) => Promise<TSample>;
5
+ processIdForConversation: (conversation: TConversation) => number | null;
6
+ processUsageEqual: (left: TSample, right: TSample) => boolean;
7
+ }
8
+
9
+ interface RefreshProcessUsageSnapshotsResult {
10
+ readonly samples: number;
11
+ readonly changed: boolean;
12
+ }
13
+
14
+ export async function refreshProcessUsageSnapshots<TConversation, TSample>(
15
+ options: RefreshProcessUsageSnapshotsOptions<TConversation, TSample>,
16
+ ): Promise<RefreshProcessUsageSnapshotsResult> {
17
+ const {
18
+ conversations,
19
+ processUsageBySessionId,
20
+ readProcessUsageSample,
21
+ processIdForConversation,
22
+ processUsageEqual,
23
+ } = options;
24
+ const entries = await Promise.all(
25
+ [...conversations.entries()].map(async ([sessionId, conversation]) => ({
26
+ sessionId,
27
+ sample: await readProcessUsageSample(processIdForConversation(conversation)),
28
+ })),
29
+ );
30
+
31
+ let changed = false;
32
+ const observedSessionIds = new Set<string>();
33
+ for (const entry of entries) {
34
+ observedSessionIds.add(entry.sessionId);
35
+ const previous = processUsageBySessionId.get(entry.sessionId);
36
+ if (previous === undefined || !processUsageEqual(previous, entry.sample)) {
37
+ processUsageBySessionId.set(entry.sessionId, entry.sample);
38
+ changed = true;
39
+ }
40
+ }
41
+ for (const sessionId of processUsageBySessionId.keys()) {
42
+ if (observedSessionIds.has(sessionId)) {
43
+ continue;
44
+ }
45
+ processUsageBySessionId.delete(sessionId);
46
+ changed = true;
47
+ }
48
+
49
+ return {
50
+ samples: entries.length,
51
+ changed,
52
+ };
53
+ }
@@ -0,0 +1,44 @@
1
+ interface HandleProjectPaneActionClickOptions<TSnapshot extends { directoryId: string }> {
2
+ clickEligible: boolean;
3
+ snapshot: TSnapshot | null;
4
+ rightCols: number;
5
+ paneRows: number;
6
+ projectPaneScrollTop: number;
7
+ rowIndex: number;
8
+ projectPaneActionAtRow: (
9
+ snapshot: TSnapshot,
10
+ rightCols: number,
11
+ paneRows: number,
12
+ projectPaneScrollTop: number,
13
+ rowIndex: number,
14
+ ) => string | null;
15
+ openNewThreadPrompt: (directoryId: string) => void;
16
+ queueCloseDirectory: (directoryId: string) => void;
17
+ markDirty: () => void;
18
+ }
19
+
20
+ export function handleProjectPaneActionClick<TSnapshot extends { directoryId: string }>(
21
+ options: HandleProjectPaneActionClickOptions<TSnapshot>,
22
+ ): boolean {
23
+ if (!options.clickEligible || options.snapshot === null) {
24
+ return false;
25
+ }
26
+ const action = options.projectPaneActionAtRow(
27
+ options.snapshot,
28
+ options.rightCols,
29
+ options.paneRows,
30
+ options.projectPaneScrollTop,
31
+ options.rowIndex,
32
+ );
33
+ if (action === 'conversation.new') {
34
+ options.openNewThreadPrompt(options.snapshot.directoryId);
35
+ options.markDirty();
36
+ return true;
37
+ }
38
+ if (action === 'project.close') {
39
+ options.queueCloseDirectory(options.snapshot.directoryId);
40
+ options.markDirty();
41
+ return true;
42
+ }
43
+ return false;
44
+ }