@jmoyers/harness 0.1.10 → 0.1.11

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 (32) hide show
  1. package/README.md +6 -2
  2. package/package.json +1 -1
  3. package/scripts/codex-live-mux-runtime.ts +162 -11
  4. package/scripts/control-plane-daemon.ts +13 -2
  5. package/scripts/harness.ts +16 -4
  6. package/src/cli/default-gateway-pointer.ts +193 -0
  7. package/src/config/config-core.ts +13 -2
  8. package/src/config/harness-paths.ts +4 -7
  9. package/src/config/harness-runtime-migration.ts +142 -19
  10. package/src/config/secrets-core.ts +92 -4
  11. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  12. package/src/control-plane/stream-server-background.ts +18 -2
  13. package/src/control-plane/stream-server.ts +79 -10
  14. package/src/domain/conversations.ts +11 -7
  15. package/src/domain/workspace.ts +9 -0
  16. package/src/mux/input-shortcuts.ts +29 -1
  17. package/src/mux/live-mux/git-parsing.ts +16 -0
  18. package/src/mux/live-mux/left-rail-conversation-click.ts +6 -3
  19. package/src/mux/live-mux/modal-input-reducers.ts +34 -1
  20. package/src/mux/live-mux/modal-overlays.ts +45 -0
  21. package/src/mux/live-mux/modal-prompt-handlers.ts +85 -0
  22. package/src/mux/task-screen-keybindings.ts +29 -1
  23. package/src/services/runtime-conversation-activation.ts +25 -0
  24. package/src/services/runtime-conversation-starter.ts +31 -7
  25. package/src/services/runtime-input-router.ts +6 -0
  26. package/src/services/runtime-modal-input.ts +18 -0
  27. package/src/services/runtime-rail-input.ts +1 -0
  28. package/src/services/runtime-repository-actions.ts +2 -0
  29. package/src/store/control-plane-store.ts +36 -0
  30. package/src/store/event-store.ts +36 -0
  31. package/src/ui/input.ts +31 -0
  32. package/src/ui/modals/manager.ts +26 -0
@@ -270,6 +270,7 @@ export class RuntimeRailInput {
270
270
 
271
271
  private openAddDirectoryPrompt(): void {
272
272
  this.options.workspace.repositoryPrompt = null;
273
+ this.options.workspace.apiKeyPrompt = null;
273
274
  this.options.workspace.addDirectoryPrompt = {
274
275
  value: '',
275
276
  error: null,
@@ -55,6 +55,7 @@ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape>
55
55
  openRepositoryPromptForCreate(): void {
56
56
  this.options.workspace.newThreadPrompt = null;
57
57
  this.options.workspace.addDirectoryPrompt = null;
58
+ this.options.workspace.apiKeyPrompt = null;
58
59
  if (this.options.workspace.conversationTitleEdit !== null) {
59
60
  this.options.stopConversationTitleEdit();
60
61
  }
@@ -75,6 +76,7 @@ export class RuntimeRepositoryActions<TRepository extends RepositoryRecordShape>
75
76
  }
76
77
  this.options.workspace.newThreadPrompt = null;
77
78
  this.options.workspace.addDirectoryPrompt = null;
79
+ this.options.workspace.apiKeyPrompt = null;
78
80
  if (this.options.workspace.conversationTitleEdit !== null) {
79
81
  this.options.stopConversationTitleEdit();
80
82
  }
@@ -355,6 +355,8 @@ interface ListGitHubSyncStateQuery {
355
355
  limit?: number;
356
356
  }
357
357
 
358
+ const CONTROL_PLANE_SCHEMA_VERSION = 1;
359
+
358
360
  export class SqliteControlPlaneStore {
359
361
  private readonly db: DatabaseSync;
360
362
 
@@ -2628,6 +2630,24 @@ export class SqliteControlPlaneStore {
2628
2630
  }
2629
2631
 
2630
2632
  private initializeSchema(): void {
2633
+ this.db.exec('BEGIN IMMEDIATE TRANSACTION');
2634
+ try {
2635
+ const currentVersion = this.readSchemaVersion();
2636
+ if (currentVersion > CONTROL_PLANE_SCHEMA_VERSION) {
2637
+ throw new Error(
2638
+ `control-plane schema version ${String(currentVersion)} is newer than supported version ${String(CONTROL_PLANE_SCHEMA_VERSION)}`,
2639
+ );
2640
+ }
2641
+ this.applySchemaV1();
2642
+ this.writeSchemaVersion(CONTROL_PLANE_SCHEMA_VERSION);
2643
+ this.db.exec('COMMIT');
2644
+ } catch (error) {
2645
+ this.db.exec('ROLLBACK');
2646
+ throw error;
2647
+ }
2648
+ }
2649
+
2650
+ private applySchemaV1(): void {
2631
2651
  this.db.exec(`
2632
2652
  CREATE TABLE IF NOT EXISTS directories (
2633
2653
  directory_id TEXT PRIMARY KEY,
@@ -2923,6 +2943,22 @@ export class SqliteControlPlaneStore {
2923
2943
  `);
2924
2944
  }
2925
2945
 
2946
+ private readSchemaVersion(): number {
2947
+ const row = this.db.prepare('PRAGMA user_version;').get();
2948
+ if (row === undefined) {
2949
+ throw new Error('failed to read control-plane schema version');
2950
+ }
2951
+ const version = (row as Record<string, unknown>)['user_version'];
2952
+ if (typeof version !== 'number' || !Number.isInteger(version) || version < 0) {
2953
+ throw new Error(`invalid control-plane schema version value: ${String(version)}`);
2954
+ }
2955
+ return version;
2956
+ }
2957
+
2958
+ private writeSchemaVersion(version: number): void {
2959
+ this.db.exec(`PRAGMA user_version = ${String(version)};`);
2960
+ }
2961
+
2926
2962
  private configureConnection(): void {
2927
2963
  this.db.exec('PRAGMA journal_mode = WAL;');
2928
2964
  this.db.exec('PRAGMA synchronous = NORMAL;');
@@ -31,6 +31,8 @@ interface PersistedEvent {
31
31
  event: NormalizedEventEnvelope;
32
32
  }
33
33
 
34
+ const EVENT_STORE_SCHEMA_VERSION = 1;
35
+
34
36
  function asObject(value: unknown): Record<string, unknown> {
35
37
  if (typeof value !== 'object' || value === null) {
36
38
  throw new Error('expected object row');
@@ -214,6 +216,24 @@ export class SqliteEventStore {
214
216
  }
215
217
 
216
218
  private initializeSchema(): void {
219
+ this.db.exec('BEGIN IMMEDIATE TRANSACTION');
220
+ try {
221
+ const currentVersion = this.readSchemaVersion();
222
+ if (currentVersion > EVENT_STORE_SCHEMA_VERSION) {
223
+ throw new Error(
224
+ `event store schema version ${String(currentVersion)} is newer than supported version ${String(EVENT_STORE_SCHEMA_VERSION)}`,
225
+ );
226
+ }
227
+ this.applySchemaV1();
228
+ this.writeSchemaVersion(EVENT_STORE_SCHEMA_VERSION);
229
+ this.db.exec('COMMIT');
230
+ } catch (error) {
231
+ this.db.exec('ROLLBACK');
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ private applySchemaV1(): void {
217
237
  this.db.exec(`
218
238
  CREATE TABLE IF NOT EXISTS events (
219
239
  row_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -236,6 +256,22 @@ export class SqliteEventStore {
236
256
  `);
237
257
  }
238
258
 
259
+ private readSchemaVersion(): number {
260
+ const row = this.db.prepare('PRAGMA user_version;').get();
261
+ if (row === undefined) {
262
+ throw new Error('failed to read event store schema version');
263
+ }
264
+ const version = (row as Record<string, unknown>)['user_version'];
265
+ if (typeof version !== 'number' || !Number.isInteger(version) || version < 0) {
266
+ throw new Error(`invalid event store schema version value: ${String(version)}`);
267
+ }
268
+ return version;
269
+ }
270
+
271
+ private writeSchemaVersion(version: number): void {
272
+ this.db.exec(`PRAGMA user_version = ${String(version)};`);
273
+ }
274
+
239
275
  private configureConnection(): void {
240
276
  this.db.exec('PRAGMA journal_mode = WAL;');
241
277
  this.db.exec('PRAGMA synchronous = NORMAL;');
package/src/ui/input.ts CHANGED
@@ -4,11 +4,13 @@ import {
4
4
  handleNewThreadPromptInput as handleNewThreadPromptInputFrame,
5
5
  } from '../mux/live-mux/modal-conversation-handlers.ts';
6
6
  import {
7
+ handleApiKeyPromptInput as handleApiKeyPromptInputFrame,
7
8
  handleAddDirectoryPromptInput as handleAddDirectoryPromptInputFrame,
8
9
  handleRepositoryPromptInput as handleRepositoryPromptInputFrame,
9
10
  } from '../mux/live-mux/modal-prompt-handlers.ts';
10
11
  import { handleTaskEditorPromptInput as handleTaskEditorPromptInputFrame } from '../mux/live-mux/modal-task-editor-handler.ts';
11
12
  import type {
13
+ ApiKeyPromptState,
12
14
  ConversationTitleEditState,
13
15
  RepositoryPromptState,
14
16
  TaskEditorPromptState,
@@ -63,6 +65,9 @@ interface InputRouterOptions {
63
65
  readonly getTaskEditorPrompt: () => TaskEditorPromptState | null;
64
66
  readonly setTaskEditorPrompt: (next: TaskEditorPromptState | null) => void;
65
67
  readonly submitTaskEditorPayload: (payload: TaskEditorSubmitPayload) => void;
68
+ readonly getApiKeyPrompt?: () => ApiKeyPromptState | null;
69
+ readonly setApiKeyPrompt?: (next: ApiKeyPromptState | null) => void;
70
+ readonly persistApiKey?: (keyName: string, value: string) => void;
66
71
  readonly getConversationTitleEdit: () => ConversationTitleEditState | null;
67
72
  readonly getCommandMenu: () => CommandMenuState | null;
68
73
  readonly setCommandMenu: (menu: CommandMenuState | null) => void;
@@ -84,6 +89,7 @@ interface InputRouterOptions {
84
89
  interface InputRouterDependencies {
85
90
  readonly handleCommandMenuInput?: typeof handleCommandMenuInputFrame;
86
91
  readonly handleTaskEditorPromptInput?: typeof handleTaskEditorPromptInputFrame;
92
+ readonly handleApiKeyPromptInput?: typeof handleApiKeyPromptInputFrame;
87
93
  readonly handleConversationTitleEditInput?: typeof handleConversationTitleEditInputFrame;
88
94
  readonly handleNewThreadPromptInput?: typeof handleNewThreadPromptInputFrame;
89
95
  readonly handleAddDirectoryPromptInput?: typeof handleAddDirectoryPromptInputFrame;
@@ -93,6 +99,7 @@ interface InputRouterDependencies {
93
99
  export class InputRouter {
94
100
  private readonly handleCommandMenuInputFrame: typeof handleCommandMenuInputFrame;
95
101
  private readonly handleTaskEditorPromptInputFrame: typeof handleTaskEditorPromptInputFrame;
102
+ private readonly handleApiKeyPromptInputFrame: typeof handleApiKeyPromptInputFrame;
96
103
  private readonly handleConversationTitleEditInputFrame: typeof handleConversationTitleEditInputFrame;
97
104
  private readonly handleNewThreadPromptInputFrame: typeof handleNewThreadPromptInputFrame;
98
105
  private readonly handleAddDirectoryPromptInputFrame: typeof handleAddDirectoryPromptInputFrame;
@@ -106,6 +113,8 @@ export class InputRouter {
106
113
  dependencies.handleCommandMenuInput ?? handleCommandMenuInputFrame;
107
114
  this.handleTaskEditorPromptInputFrame =
108
115
  dependencies.handleTaskEditorPromptInput ?? handleTaskEditorPromptInputFrame;
116
+ this.handleApiKeyPromptInputFrame =
117
+ dependencies.handleApiKeyPromptInput ?? handleApiKeyPromptInputFrame;
109
118
  this.handleConversationTitleEditInputFrame =
110
119
  dependencies.handleConversationTitleEditInput ?? handleConversationTitleEditInputFrame;
111
120
  this.handleNewThreadPromptInputFrame =
@@ -168,6 +177,25 @@ export class InputRouter {
168
177
  });
169
178
  }
170
179
 
180
+ handleApiKeyPromptInput(input: Buffer): boolean {
181
+ if (
182
+ this.options.getApiKeyPrompt === undefined ||
183
+ this.options.setApiKeyPrompt === undefined ||
184
+ this.options.persistApiKey === undefined
185
+ ) {
186
+ return false;
187
+ }
188
+ return this.handleApiKeyPromptInputFrame({
189
+ input,
190
+ prompt: this.options.getApiKeyPrompt(),
191
+ isQuitShortcut: this.options.isModalDismissShortcut,
192
+ dismissOnOutsideClick: this.options.dismissOnOutsideClick,
193
+ setPrompt: this.options.setApiKeyPrompt,
194
+ markDirty: this.options.markDirty,
195
+ persistApiKey: this.options.persistApiKey,
196
+ });
197
+ }
198
+
171
199
  handleNewThreadPromptInput(input: Buffer): boolean {
172
200
  return this.handleNewThreadPromptInputFrame({
173
201
  input,
@@ -224,6 +252,9 @@ export class InputRouter {
224
252
  if (this.handleRepositoryPromptInput(input)) {
225
253
  return true;
226
254
  }
255
+ if (this.handleApiKeyPromptInput(input)) {
256
+ return true;
257
+ }
227
258
  if (this.handleNewThreadPromptInput(input)) {
228
259
  return true;
229
260
  }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  buildCommandMenuModalOverlay as buildCommandMenuModalOverlayFrame,
3
3
  buildAddDirectoryModalOverlay as buildAddDirectoryModalOverlayFrame,
4
+ buildApiKeyModalOverlay as buildApiKeyModalOverlayFrame,
4
5
  buildConversationTitleModalOverlay as buildConversationTitleModalOverlayFrame,
5
6
  buildNewThreadModalOverlay as buildNewThreadModalOverlayFrame,
6
7
  buildRepositoryModalOverlay as buildRepositoryModalOverlayFrame,
@@ -21,6 +22,13 @@ import { isUiModalOverlayHit } from '../kit.ts';
21
22
 
22
23
  type NewThreadPromptState = ReturnType<typeof createNewThreadPromptState>;
23
24
  type AddDirectoryPromptState = { value: string; error: string | null };
25
+ type ApiKeyPromptState = {
26
+ keyName: string;
27
+ displayName: string;
28
+ value: string;
29
+ error: string | null;
30
+ hasExistingValue: boolean;
31
+ };
24
32
  type ModalOverlay = Exclude<ReturnType<typeof buildNewThreadModalOverlayFrame>, null>;
25
33
  type ModalTheme = Parameters<typeof buildNewThreadModalOverlayFrame>[3];
26
34
  type DismissModalOnOutsideClickInput = Parameters<typeof dismissModalOnOutsideClickFrame>[0];
@@ -32,6 +40,7 @@ interface ModalManagerOptions {
32
40
  readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
33
41
  readonly getNewThreadPrompt: () => NewThreadPromptState | null;
34
42
  readonly getAddDirectoryPrompt: () => AddDirectoryPromptState | null;
43
+ readonly getApiKeyPrompt?: () => ApiKeyPromptState | null;
35
44
  readonly getTaskEditorPrompt: () => TaskEditorPromptState | null;
36
45
  readonly getRepositoryPrompt: () => RepositoryPromptState | null;
37
46
  readonly getConversationTitleEdit: () => ConversationTitleEditState | null;
@@ -42,6 +51,7 @@ interface ModalManagerDependencies {
42
51
  readonly buildNewThreadModalOverlay?: typeof buildNewThreadModalOverlayFrame;
43
52
  readonly buildAddDirectoryModalOverlay?: typeof buildAddDirectoryModalOverlayFrame;
44
53
  readonly buildTaskEditorModalOverlay?: typeof buildTaskEditorModalOverlayFrame;
54
+ readonly buildApiKeyModalOverlay?: typeof buildApiKeyModalOverlayFrame;
45
55
  readonly buildRepositoryModalOverlay?: typeof buildRepositoryModalOverlayFrame;
46
56
  readonly buildConversationTitleModalOverlay?: typeof buildConversationTitleModalOverlayFrame;
47
57
  readonly dismissModalOnOutsideClick?: typeof dismissModalOnOutsideClickFrame;
@@ -67,6 +77,7 @@ export class ModalManager {
67
77
  private readonly buildNewThreadModalOverlay: typeof buildNewThreadModalOverlayFrame;
68
78
  private readonly buildAddDirectoryModalOverlay: typeof buildAddDirectoryModalOverlayFrame;
69
79
  private readonly buildTaskEditorModalOverlay: typeof buildTaskEditorModalOverlayFrame;
80
+ private readonly buildApiKeyModalOverlay: typeof buildApiKeyModalOverlayFrame;
70
81
  private readonly buildRepositoryModalOverlay: typeof buildRepositoryModalOverlayFrame;
71
82
  private readonly buildConversationTitleModalOverlay: typeof buildConversationTitleModalOverlayFrame;
72
83
  private readonly dismissModalOnOutsideClick: typeof dismissModalOnOutsideClickFrame;
@@ -84,6 +95,8 @@ export class ModalManager {
84
95
  dependencies.buildAddDirectoryModalOverlay ?? buildAddDirectoryModalOverlayFrame;
85
96
  this.buildTaskEditorModalOverlay =
86
97
  dependencies.buildTaskEditorModalOverlay ?? buildTaskEditorModalOverlayFrame;
98
+ this.buildApiKeyModalOverlay =
99
+ dependencies.buildApiKeyModalOverlay ?? buildApiKeyModalOverlayFrame;
87
100
  this.buildRepositoryModalOverlay =
88
101
  dependencies.buildRepositoryModalOverlay ?? buildRepositoryModalOverlayFrame;
89
102
  this.buildConversationTitleModalOverlay =
@@ -131,6 +144,15 @@ export class ModalManager {
131
144
  );
132
145
  }
133
146
 
147
+ buildApiKeyOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
148
+ return this.buildApiKeyModalOverlay(
149
+ layoutCols,
150
+ viewportRows,
151
+ this.options.getApiKeyPrompt?.() ?? null,
152
+ this.options.theme,
153
+ );
154
+ }
155
+
134
156
  buildRepositoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
135
157
  return this.buildRepositoryModalOverlay(
136
158
  layoutCols,
@@ -166,6 +188,10 @@ export class ModalManager {
166
188
  if (taskEditorOverlay !== null) {
167
189
  return taskEditorOverlay;
168
190
  }
191
+ const apiKeyOverlay = this.buildApiKeyOverlay(layoutCols, viewportRows);
192
+ if (apiKeyOverlay !== null) {
193
+ return apiKeyOverlay;
194
+ }
169
195
  const repositoryOverlay = this.buildRepositoryOverlay(layoutCols, viewportRows);
170
196
  if (repositoryOverlay !== null) {
171
197
  return repositoryOverlay;