@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.
- package/README.md +6 -2
- package/package.json +1 -1
- package/scripts/codex-live-mux-runtime.ts +162 -11
- package/scripts/control-plane-daemon.ts +13 -2
- package/scripts/harness.ts +16 -4
- package/src/cli/default-gateway-pointer.ts +193 -0
- package/src/config/config-core.ts +13 -2
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/prompt/thread-title-namer.ts +49 -23
- package/src/control-plane/stream-server-background.ts +18 -2
- package/src/control-plane/stream-server.ts +79 -10
- package/src/domain/conversations.ts +11 -7
- package/src/domain/workspace.ts +9 -0
- package/src/mux/input-shortcuts.ts +29 -1
- package/src/mux/live-mux/git-parsing.ts +16 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +6 -3
- package/src/mux/live-mux/modal-input-reducers.ts +34 -1
- package/src/mux/live-mux/modal-overlays.ts +45 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +85 -0
- package/src/mux/task-screen-keybindings.ts +29 -1
- package/src/services/runtime-conversation-activation.ts +25 -0
- package/src/services/runtime-conversation-starter.ts +31 -7
- package/src/services/runtime-input-router.ts +6 -0
- package/src/services/runtime-modal-input.ts +18 -0
- package/src/services/runtime-rail-input.ts +1 -0
- package/src/services/runtime-repository-actions.ts +2 -0
- package/src/store/control-plane-store.ts +36 -0
- package/src/store/event-store.ts +36 -0
- package/src/ui/input.ts +31 -0
- 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;');
|
package/src/store/event-store.ts
CHANGED
|
@@ -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
|
}
|
package/src/ui/modals/manager.ts
CHANGED
|
@@ -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;
|