@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
|
@@ -1381,7 +1381,18 @@ export class ControlPlaneStreamServer {
|
|
|
1381
1381
|
}
|
|
1382
1382
|
|
|
1383
1383
|
private pollHistoryTimerTick(): void {
|
|
1384
|
-
void this.pollHistoryFile()
|
|
1384
|
+
void this.pollHistoryFile().catch((error: unknown) => {
|
|
1385
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (this.shouldSkipStateStoreWork()) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1392
|
+
recordPerfEvent('control-plane.history.poll.failed', {
|
|
1393
|
+
error: message,
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1385
1396
|
}
|
|
1386
1397
|
|
|
1387
1398
|
private stopHistoryPolling(): void {
|
|
@@ -1399,9 +1410,9 @@ export class ControlPlaneStreamServer {
|
|
|
1399
1410
|
return;
|
|
1400
1411
|
}
|
|
1401
1412
|
this.reloadGitStatusDirectoriesFromStore();
|
|
1402
|
-
|
|
1413
|
+
this.triggerGitStatusPoll();
|
|
1403
1414
|
this.gitStatusPollTimer = setInterval(() => {
|
|
1404
|
-
|
|
1415
|
+
this.triggerGitStatusPoll();
|
|
1405
1416
|
}, this.gitStatusMonitor.pollMs);
|
|
1406
1417
|
this.gitStatusPollTimer.unref();
|
|
1407
1418
|
}
|
|
@@ -1494,6 +1505,9 @@ export class ControlPlaneStreamServer {
|
|
|
1494
1505
|
|
|
1495
1506
|
private triggerGitHubPoll(): void {
|
|
1496
1507
|
void this.pollGitHub().catch((error: unknown) => {
|
|
1508
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1497
1511
|
if (this.shouldIgnoreGitHubPollError(error)) {
|
|
1498
1512
|
return;
|
|
1499
1513
|
}
|
|
@@ -1504,10 +1518,40 @@ export class ControlPlaneStreamServer {
|
|
|
1504
1518
|
});
|
|
1505
1519
|
}
|
|
1506
1520
|
|
|
1521
|
+
private triggerGitStatusPoll(): void {
|
|
1522
|
+
void this.pollGitStatus().catch((error: unknown) => {
|
|
1523
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (this.shouldSkipStateStoreWork()) {
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1530
|
+
recordPerfEvent('control-plane.git-status.poll.failed', {
|
|
1531
|
+
error: message,
|
|
1532
|
+
});
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1507
1536
|
private isStateStoreClosedError(error: unknown): boolean {
|
|
1508
1537
|
const message = error instanceof Error ? error.message : String(error);
|
|
1509
1538
|
const normalized = message.trim().toLowerCase();
|
|
1510
|
-
return
|
|
1539
|
+
return (
|
|
1540
|
+
normalized.includes('database has closed') ||
|
|
1541
|
+
normalized.includes('database is closed') ||
|
|
1542
|
+
normalized.includes('cannot use a closed database')
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
private markStateStoreClosedIfDetected(error: unknown): boolean {
|
|
1547
|
+
if (!this.isStateStoreClosedError(error)) {
|
|
1548
|
+
return false;
|
|
1549
|
+
}
|
|
1550
|
+
this.stateStoreClosed = true;
|
|
1551
|
+
this.stopGitHubPolling();
|
|
1552
|
+
this.stopGitStatusPolling();
|
|
1553
|
+
this.stopHistoryPolling();
|
|
1554
|
+
return true;
|
|
1511
1555
|
}
|
|
1512
1556
|
|
|
1513
1557
|
private shouldSkipStateStoreWork(): boolean {
|
|
@@ -1526,6 +1570,9 @@ export class ControlPlaneStreamServer {
|
|
|
1526
1570
|
try {
|
|
1527
1571
|
await pollPromise;
|
|
1528
1572
|
} catch (error: unknown) {
|
|
1573
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1529
1576
|
if (!this.shouldIgnoreGitHubPollError(error)) {
|
|
1530
1577
|
const message = error instanceof Error ? error.message : String(error);
|
|
1531
1578
|
recordPerfEvent('control-plane.github.poll.failed-on-close', {
|
|
@@ -2205,9 +2252,15 @@ export class ControlPlaneStreamServer {
|
|
|
2205
2252
|
}
|
|
2206
2253
|
|
|
2207
2254
|
private async pollHistoryFile(): Promise<void> {
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2255
|
+
try {
|
|
2256
|
+
await pollStreamServerHistoryFile(
|
|
2257
|
+
this as unknown as Parameters<typeof pollStreamServerHistoryFile>[0],
|
|
2258
|
+
);
|
|
2259
|
+
} catch (error: unknown) {
|
|
2260
|
+
if (!this.markStateStoreClosedIfDetected(error)) {
|
|
2261
|
+
throw error;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2211
2264
|
}
|
|
2212
2265
|
|
|
2213
2266
|
private async pollHistoryFileUnsafe(): Promise<boolean> {
|
|
@@ -2217,9 +2270,15 @@ export class ControlPlaneStreamServer {
|
|
|
2217
2270
|
}
|
|
2218
2271
|
|
|
2219
2272
|
private async pollGitStatus(): Promise<void> {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2273
|
+
try {
|
|
2274
|
+
await pollStreamServerGitStatus(
|
|
2275
|
+
this as unknown as Parameters<typeof pollStreamServerGitStatus>[0],
|
|
2276
|
+
);
|
|
2277
|
+
} catch (error: unknown) {
|
|
2278
|
+
if (!this.markStateStoreClosedIfDetected(error)) {
|
|
2279
|
+
throw error;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2223
2282
|
}
|
|
2224
2283
|
|
|
2225
2284
|
private async refreshGitStatusForDirectory(
|
|
@@ -2314,6 +2373,10 @@ export class ControlPlaneStreamServer {
|
|
|
2314
2373
|
this.githubPollPromise = pollPromise;
|
|
2315
2374
|
try {
|
|
2316
2375
|
await pollPromise;
|
|
2376
|
+
} catch (error: unknown) {
|
|
2377
|
+
if (!this.markStateStoreClosedIfDetected(error)) {
|
|
2378
|
+
throw error;
|
|
2379
|
+
}
|
|
2317
2380
|
} finally {
|
|
2318
2381
|
if (this.githubPollPromise === pollPromise) {
|
|
2319
2382
|
this.githubPollPromise = null;
|
|
@@ -2491,6 +2554,9 @@ export class ControlPlaneStreamServer {
|
|
|
2491
2554
|
lastErrorAt: null,
|
|
2492
2555
|
});
|
|
2493
2556
|
} catch (error: unknown) {
|
|
2557
|
+
if (this.markStateStoreClosedIfDetected(error)) {
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2494
2560
|
if (this.shouldIgnoreGitHubPollError(error)) {
|
|
2495
2561
|
return;
|
|
2496
2562
|
}
|
|
@@ -2510,6 +2576,9 @@ export class ControlPlaneStreamServer {
|
|
|
2510
2576
|
lastErrorAt: now,
|
|
2511
2577
|
});
|
|
2512
2578
|
} catch (syncStateError: unknown) {
|
|
2579
|
+
if (this.markStateStoreClosedIfDetected(syncStateError)) {
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2513
2582
|
if (!this.shouldIgnoreGitHubPollError(syncStateError)) {
|
|
2514
2583
|
throw syncStateError;
|
|
2515
2584
|
}
|
|
@@ -290,6 +290,8 @@ export class ConversationManager {
|
|
|
290
290
|
|
|
291
291
|
upsertFromPersistedRecord(input: UpsertPersistedConversationInput): ConversationState {
|
|
292
292
|
const { record } = input;
|
|
293
|
+
const existing = this.conversationsBySessionId.get(record.conversationId);
|
|
294
|
+
const preserveLiveRuntime = existing?.live === true;
|
|
293
295
|
const conversation = input.ensureConversation(record.conversationId, {
|
|
294
296
|
directoryId: record.directoryId,
|
|
295
297
|
title: record.title,
|
|
@@ -299,14 +301,16 @@ export class ConversationManager {
|
|
|
299
301
|
conversation.scope.tenantId = record.tenantId;
|
|
300
302
|
conversation.scope.userId = record.userId;
|
|
301
303
|
conversation.scope.workspaceId = record.workspaceId;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
304
|
+
if (!preserveLiveRuntime) {
|
|
305
|
+
const runtimeStatusModel = record.runtimeStatusModel;
|
|
306
|
+
conversation.status = record.runtimeStatus;
|
|
307
|
+
conversation.statusModel = runtimeStatusModel;
|
|
308
|
+
conversation.attentionReason = runtimeStatusModel?.attentionReason ?? null;
|
|
309
|
+
conversation.lastKnownWork = runtimeStatusModel?.lastKnownWork ?? null;
|
|
310
|
+
conversation.lastKnownWorkAt = runtimeStatusModel?.lastKnownWorkAt ?? null;
|
|
311
|
+
}
|
|
308
312
|
// Persisted runtime flags are advisory; session.list is authoritative for live sessions.
|
|
309
|
-
conversation.live = false;
|
|
313
|
+
conversation.live = preserveLiveRuntime ? true : false;
|
|
310
314
|
return conversation;
|
|
311
315
|
}
|
|
312
316
|
|
package/src/domain/workspace.ts
CHANGED
|
@@ -25,6 +25,14 @@ export interface RepositoryPromptState {
|
|
|
25
25
|
readonly error: string | null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface ApiKeyPromptState {
|
|
29
|
+
readonly keyName: string;
|
|
30
|
+
readonly displayName: string;
|
|
31
|
+
readonly value: string;
|
|
32
|
+
readonly error: string | null;
|
|
33
|
+
readonly hasExistingValue: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export interface TaskEditorPromptState {
|
|
29
37
|
mode: 'create' | 'edit';
|
|
30
38
|
taskId: string | null;
|
|
@@ -80,6 +88,7 @@ export class WorkspaceModel {
|
|
|
80
88
|
selectionDrag: PaneSelectionDrag | null = null;
|
|
81
89
|
selectionPinnedFollowOutput: boolean | null = null;
|
|
82
90
|
repositoryPrompt: RepositoryPromptState | null = null;
|
|
91
|
+
apiKeyPrompt: ApiKeyPromptState | null = null;
|
|
83
92
|
commandMenu: CommandMenuState | null = null;
|
|
84
93
|
newThreadPrompt: ReturnType<typeof createNewThreadPromptState> | null = null;
|
|
85
94
|
addDirectoryPrompt: { value: string; error: string | null } | null = null;
|
|
@@ -541,10 +541,38 @@ function strokesEqual(left: KeyStroke, right: KeyStroke): boolean {
|
|
|
541
541
|
|
|
542
542
|
function parseBindingsForAction(rawBindings: readonly string[]): readonly ParsedShortcutBinding[] {
|
|
543
543
|
const parsed: ParsedShortcutBinding[] = [];
|
|
544
|
+
|
|
545
|
+
const pushIfUnique = (candidate: ParsedShortcutBinding): void => {
|
|
546
|
+
if (parsed.some((existing) => strokesEqual(existing.stroke, candidate.stroke))) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
parsed.push(candidate);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const ctrlMetaAliasStroke = (stroke: KeyStroke): KeyStroke | null => {
|
|
553
|
+
if (stroke.ctrl === stroke.meta) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
key: stroke.key,
|
|
558
|
+
ctrl: !stroke.ctrl,
|
|
559
|
+
alt: stroke.alt,
|
|
560
|
+
shift: stroke.shift,
|
|
561
|
+
meta: !stroke.meta,
|
|
562
|
+
};
|
|
563
|
+
};
|
|
564
|
+
|
|
544
565
|
for (const raw of rawBindings) {
|
|
545
566
|
const normalized = parseShortcutBinding(raw);
|
|
546
567
|
if (normalized !== null) {
|
|
547
|
-
|
|
568
|
+
pushIfUnique(normalized);
|
|
569
|
+
const aliasStroke = ctrlMetaAliasStroke(normalized.stroke);
|
|
570
|
+
if (aliasStroke !== null) {
|
|
571
|
+
pushIfUnique({
|
|
572
|
+
stroke: aliasStroke,
|
|
573
|
+
originalText: normalized.originalText,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
548
576
|
}
|
|
549
577
|
}
|
|
550
578
|
return parsed;
|
|
@@ -82,6 +82,22 @@ export function repositoryNameFromGitHubRemoteUrl(remoteUrl: string): string {
|
|
|
82
82
|
return name;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function normalizeDefaultBranchForActions(value: string | null): string | null {
|
|
86
|
+
const trimmed = value?.trim() ?? '';
|
|
87
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveGitHubDefaultBranchForActions(input: {
|
|
91
|
+
repositoryDefaultBranch: string | null;
|
|
92
|
+
snapshotDefaultBranch: string | null;
|
|
93
|
+
}): string | null {
|
|
94
|
+
const repositoryDefaultBranch = normalizeDefaultBranchForActions(input.repositoryDefaultBranch);
|
|
95
|
+
if (repositoryDefaultBranch !== null) {
|
|
96
|
+
return repositoryDefaultBranch;
|
|
97
|
+
}
|
|
98
|
+
return normalizeDefaultBranchForActions(input.snapshotDefaultBranch);
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
export function shouldShowGitHubPrActions(input: {
|
|
86
102
|
trackedBranch: string | null;
|
|
87
103
|
defaultBranch: string | null;
|
|
@@ -46,9 +46,12 @@ export function handleLeftRailConversationClick(
|
|
|
46
46
|
options.selectedConversationId === options.activeConversationId
|
|
47
47
|
) {
|
|
48
48
|
if (!options.isConversationPaneActive) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
if (conversationClick.doubleClick) {
|
|
50
|
+
options.queueActivateConversationAndEdit(options.selectedConversationId);
|
|
51
|
+
} else {
|
|
52
|
+
options.queueActivateConversation(options.selectedConversationId);
|
|
53
|
+
}
|
|
54
|
+
} else if (conversationClick.doubleClick) {
|
|
52
55
|
options.beginConversationTitleEdit(options.selectedConversationId);
|
|
53
56
|
}
|
|
54
57
|
options.markDirty();
|
|
@@ -19,10 +19,43 @@ interface LinePromptReduction {
|
|
|
19
19
|
submit: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const BRACKETED_PASTE_START = Buffer.from('\u001b[200~', 'utf8');
|
|
23
|
+
const BRACKETED_PASTE_END = Buffer.from('\u001b[201~', 'utf8');
|
|
24
|
+
|
|
25
|
+
function matchesSequence(input: Buffer, startIndex: number, sequence: Buffer): boolean {
|
|
26
|
+
if (startIndex < 0 || startIndex + sequence.length > input.length) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
for (let index = 0; index < sequence.length; index += 1) {
|
|
30
|
+
if (input[startIndex + index] !== sequence[index]) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
export function reduceLinePromptInput(value: string, input: Buffer): LinePromptReduction {
|
|
23
38
|
let nextValue = value;
|
|
24
39
|
let submit = false;
|
|
25
|
-
|
|
40
|
+
let inBracketedPaste = false;
|
|
41
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
42
|
+
if (!inBracketedPaste && matchesSequence(input, index, BRACKETED_PASTE_START)) {
|
|
43
|
+
inBracketedPaste = true;
|
|
44
|
+
index += BRACKETED_PASTE_START.length - 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (inBracketedPaste && matchesSequence(input, index, BRACKETED_PASTE_END)) {
|
|
48
|
+
inBracketedPaste = false;
|
|
49
|
+
index += BRACKETED_PASTE_END.length - 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const byte = input[index]!;
|
|
53
|
+
if (inBracketedPaste) {
|
|
54
|
+
if (byte >= 32 && byte <= 126) {
|
|
55
|
+
nextValue += String.fromCharCode(byte);
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
26
59
|
if (byte === 0x0d || byte === 0x0a) {
|
|
27
60
|
submit = true;
|
|
28
61
|
break;
|
|
@@ -36,6 +36,14 @@ interface RepositoryPromptOverlayState {
|
|
|
36
36
|
readonly error: string | null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
interface ApiKeyPromptOverlayState {
|
|
40
|
+
readonly keyName: string;
|
|
41
|
+
readonly displayName: string;
|
|
42
|
+
readonly value: string;
|
|
43
|
+
readonly error: string | null;
|
|
44
|
+
readonly hasExistingValue: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
interface ConversationTitleOverlayState {
|
|
40
48
|
value: string;
|
|
41
49
|
lastSavedValue: string;
|
|
@@ -252,6 +260,43 @@ export function buildRepositoryModalOverlay(
|
|
|
252
260
|
});
|
|
253
261
|
}
|
|
254
262
|
|
|
263
|
+
export function buildApiKeyModalOverlay(
|
|
264
|
+
layoutCols: number,
|
|
265
|
+
viewportRows: number,
|
|
266
|
+
prompt: ApiKeyPromptOverlayState | null,
|
|
267
|
+
theme: UiModalThemeInput,
|
|
268
|
+
): ReturnType<typeof buildUiModalOverlay> | null {
|
|
269
|
+
if (prompt === null) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
|
|
273
|
+
preferredHeight: 16,
|
|
274
|
+
minWidth: 34,
|
|
275
|
+
maxWidth: 64,
|
|
276
|
+
});
|
|
277
|
+
const promptValue = prompt.value.length > 0 ? prompt.value : '(enter value)';
|
|
278
|
+
const bodyLines = [`${prompt.keyName}: ${promptValue}_`];
|
|
279
|
+
if (prompt.error !== null && prompt.error.length > 0) {
|
|
280
|
+
bodyLines.push(`error: ${prompt.error}`);
|
|
281
|
+
} else if (prompt.hasExistingValue) {
|
|
282
|
+
bodyLines.push('warning: existing value detected (submit will overwrite)');
|
|
283
|
+
} else {
|
|
284
|
+
bodyLines.push('value is saved to user-global secrets.env');
|
|
285
|
+
}
|
|
286
|
+
return buildUiModalOverlay({
|
|
287
|
+
viewportCols: layoutCols,
|
|
288
|
+
viewportRows,
|
|
289
|
+
width: modalSize.width,
|
|
290
|
+
height: modalSize.height,
|
|
291
|
+
anchor: 'center',
|
|
292
|
+
marginRows: 1,
|
|
293
|
+
title: `Set ${prompt.displayName}`,
|
|
294
|
+
bodyLines,
|
|
295
|
+
footer: 'enter save esc',
|
|
296
|
+
theme,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
255
300
|
export function buildConversationTitleModalOverlay(
|
|
256
301
|
layoutCols: number,
|
|
257
302
|
viewportRows: number,
|
|
@@ -12,6 +12,14 @@ interface RepositoryPromptState {
|
|
|
12
12
|
readonly error: string | null;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
interface ApiKeyPromptState {
|
|
16
|
+
readonly keyName: string;
|
|
17
|
+
readonly displayName: string;
|
|
18
|
+
readonly value: string;
|
|
19
|
+
readonly error: string | null;
|
|
20
|
+
readonly hasExistingValue: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
interface HandleAddDirectoryPromptInputOptions {
|
|
16
24
|
input: Buffer;
|
|
17
25
|
prompt: AddDirectoryPromptState | null;
|
|
@@ -36,6 +44,16 @@ interface HandleRepositoryPromptInputOptions {
|
|
|
36
44
|
upsertRepositoryByRemoteUrl: (remoteUrl: string, existingRepositoryId?: string) => Promise<void>;
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
interface HandleApiKeyPromptInputOptions {
|
|
48
|
+
input: Buffer;
|
|
49
|
+
prompt: ApiKeyPromptState | null;
|
|
50
|
+
isQuitShortcut: (input: Buffer) => boolean;
|
|
51
|
+
dismissOnOutsideClick: (input: Buffer, dismiss: () => void) => boolean;
|
|
52
|
+
setPrompt: (next: ApiKeyPromptState | null) => void;
|
|
53
|
+
markDirty: () => void;
|
|
54
|
+
persistApiKey: (keyName: string, value: string) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
export function handleAddDirectoryPromptInput(
|
|
40
58
|
options: HandleAddDirectoryPromptInputOptions,
|
|
41
59
|
): boolean {
|
|
@@ -185,3 +203,70 @@ export function handleRepositoryPromptInput(options: HandleRepositoryPromptInput
|
|
|
185
203
|
markDirty();
|
|
186
204
|
return true;
|
|
187
205
|
}
|
|
206
|
+
|
|
207
|
+
export function handleApiKeyPromptInput(options: HandleApiKeyPromptInputOptions): boolean {
|
|
208
|
+
const {
|
|
209
|
+
input,
|
|
210
|
+
prompt,
|
|
211
|
+
isQuitShortcut,
|
|
212
|
+
dismissOnOutsideClick,
|
|
213
|
+
setPrompt,
|
|
214
|
+
markDirty,
|
|
215
|
+
persistApiKey,
|
|
216
|
+
} = options;
|
|
217
|
+
if (prompt === null) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
if (input.length === 1 && input[0] === 0x03) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (isQuitShortcut(input)) {
|
|
224
|
+
setPrompt(null);
|
|
225
|
+
markDirty();
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
dismissOnOutsideClick(input, () => {
|
|
230
|
+
setPrompt(null);
|
|
231
|
+
markDirty();
|
|
232
|
+
})
|
|
233
|
+
) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const reduced = reduceLinePromptInput(prompt.value, input);
|
|
238
|
+
const value = reduced.value;
|
|
239
|
+
if (!reduced.submit) {
|
|
240
|
+
setPrompt({
|
|
241
|
+
...prompt,
|
|
242
|
+
value,
|
|
243
|
+
error: null,
|
|
244
|
+
});
|
|
245
|
+
markDirty();
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const trimmed = value.trim();
|
|
250
|
+
if (trimmed.length === 0) {
|
|
251
|
+
setPrompt({
|
|
252
|
+
...prompt,
|
|
253
|
+
value,
|
|
254
|
+
error: `${prompt.displayName.toLowerCase()} required`,
|
|
255
|
+
});
|
|
256
|
+
markDirty();
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
persistApiKey(prompt.keyName, trimmed);
|
|
261
|
+
setPrompt(null);
|
|
262
|
+
} catch (error: unknown) {
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
setPrompt({
|
|
265
|
+
...prompt,
|
|
266
|
+
value,
|
|
267
|
+
error: message,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
markDirty();
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
@@ -437,10 +437,38 @@ function parseBinding(input: string): ParsedBinding | null {
|
|
|
437
437
|
|
|
438
438
|
function bindingsForAction(raw: readonly string[]): readonly ParsedBinding[] {
|
|
439
439
|
const parsed: ParsedBinding[] = [];
|
|
440
|
+
|
|
441
|
+
const pushIfUnique = (candidate: ParsedBinding): void => {
|
|
442
|
+
if (parsed.some((existing) => strokesEqual(existing.stroke, candidate.stroke))) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
parsed.push(candidate);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const ctrlMetaAliasStroke = (stroke: KeyStroke): KeyStroke | null => {
|
|
449
|
+
if (stroke.ctrl === stroke.meta) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
key: stroke.key,
|
|
454
|
+
ctrl: !stroke.ctrl,
|
|
455
|
+
alt: stroke.alt,
|
|
456
|
+
shift: stroke.shift,
|
|
457
|
+
meta: !stroke.meta,
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
|
|
440
461
|
for (const value of raw) {
|
|
441
462
|
const next = parseBinding(value);
|
|
442
463
|
if (next !== null) {
|
|
443
|
-
|
|
464
|
+
pushIfUnique(next);
|
|
465
|
+
const aliasStroke = ctrlMetaAliasStroke(next.stroke);
|
|
466
|
+
if (aliasStroke !== null) {
|
|
467
|
+
pushIfUnique({
|
|
468
|
+
stroke: aliasStroke,
|
|
469
|
+
originalText: next.originalText,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
444
472
|
}
|
|
445
473
|
}
|
|
446
474
|
return parsed;
|
|
@@ -30,7 +30,32 @@ export class RuntimeConversationActivation {
|
|
|
30
30
|
async activateConversation(sessionId: string): Promise<void> {
|
|
31
31
|
if (this.options.getActiveConversationId() === sessionId) {
|
|
32
32
|
if (!this.options.isConversationPaneMode()) {
|
|
33
|
+
const targetConversation = this.options.conversationById(sessionId);
|
|
33
34
|
this.options.enterConversationPaneForActiveSession(sessionId);
|
|
35
|
+
this.options.noteGitActivity(targetConversation?.directoryId ?? null);
|
|
36
|
+
if (
|
|
37
|
+
targetConversation !== undefined &&
|
|
38
|
+
!targetConversation.live &&
|
|
39
|
+
targetConversation.status !== 'exited'
|
|
40
|
+
) {
|
|
41
|
+
await this.options.startConversation(sessionId);
|
|
42
|
+
}
|
|
43
|
+
if (targetConversation?.status !== 'exited') {
|
|
44
|
+
try {
|
|
45
|
+
await this.options.attachConversation(sessionId);
|
|
46
|
+
} catch (error: unknown) {
|
|
47
|
+
if (
|
|
48
|
+
!this.options.isSessionNotFoundError(error) &&
|
|
49
|
+
!this.options.isSessionNotLiveError(error)
|
|
50
|
+
) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
this.options.markSessionUnavailable(sessionId);
|
|
54
|
+
await this.options.startConversation(sessionId);
|
|
55
|
+
await this.options.attachConversation(sessionId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.options.schedulePtyResizeImmediate();
|
|
34
59
|
this.options.markDirty();
|
|
35
60
|
}
|
|
36
61
|
return;
|
|
@@ -33,6 +33,13 @@ interface RuntimeConversationStarterLaunchArgsInput {
|
|
|
33
33
|
|
|
34
34
|
type RuntimeConversationStarterSpanAttributes = Record<string, string | number | boolean>;
|
|
35
35
|
|
|
36
|
+
function isSessionAlreadyExistsError(error: unknown): boolean {
|
|
37
|
+
if (!(error instanceof Error)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return error.message.includes('session already exists');
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
export interface RuntimeConversationStarterOptions<
|
|
37
44
|
TConversation extends RuntimeConversationStarterConversationRecord,
|
|
38
45
|
TSessionSummary,
|
|
@@ -134,19 +141,36 @@ export class RuntimeConversationStarter<
|
|
|
134
141
|
if (this.options.terminalBackgroundHex !== undefined) {
|
|
135
142
|
ptyStartInput.terminalBackgroundHex = this.options.terminalBackgroundHex;
|
|
136
143
|
}
|
|
137
|
-
|
|
144
|
+
let startedSession = false;
|
|
145
|
+
try {
|
|
146
|
+
await this.options.startPtySession(ptyStartInput);
|
|
147
|
+
startedSession = true;
|
|
148
|
+
} catch (error: unknown) {
|
|
149
|
+
if (!isSessionAlreadyExistsError(error)) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
138
153
|
this.options.setPtySize(sessionId, {
|
|
139
154
|
cols: layout.rightCols,
|
|
140
155
|
rows: layout.paneRows,
|
|
141
156
|
});
|
|
142
157
|
this.options.sendResize(sessionId, layout.rightCols, layout.paneRows);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
if (startedSession) {
|
|
159
|
+
this.endStartCommandSpanIfTarget(sessionId, {
|
|
160
|
+
alreadyLive: false,
|
|
161
|
+
argCount: launchArgs.length,
|
|
162
|
+
resumed: launchArgs[0] === 'resume',
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
this.endStartCommandSpanIfTarget(sessionId, {
|
|
166
|
+
alreadyLive: true,
|
|
167
|
+
recoveredDuplicateStart: true,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
148
170
|
const state = this.options.ensureConversation(sessionId);
|
|
149
|
-
|
|
171
|
+
if (startedSession) {
|
|
172
|
+
this.options.recordStartCommand(sessionId, launchArgs);
|
|
173
|
+
}
|
|
150
174
|
const statusSummary = await this.options.getSessionStatus(sessionId);
|
|
151
175
|
if (statusSummary !== null) {
|
|
152
176
|
this.options.upsertFromSessionSummary(statusSummary);
|
|
@@ -41,6 +41,7 @@ interface RuntimeInputRouterOptions {
|
|
|
41
41
|
readonly scheduleConversationTitlePersist: () => void;
|
|
42
42
|
readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
|
|
43
43
|
readonly executeCommandMenuAction: (actionId: string) => void;
|
|
44
|
+
readonly persistApiKey?: RuntimeModalInputOptions['persistApiKey'];
|
|
44
45
|
readonly requestStop: RuntimeRailInputOptions['requestStop'];
|
|
45
46
|
readonly resolveDirectoryForAction: RuntimeRailInputOptions['resolveDirectoryForAction'];
|
|
46
47
|
readonly toggleCommandMenu: RuntimeRailInputOptions['toggleCommandMenu'];
|
|
@@ -112,6 +113,11 @@ export class RuntimeInputRouter {
|
|
|
112
113
|
scheduleConversationTitlePersist: options.scheduleConversationTitlePersist,
|
|
113
114
|
resolveCommandMenuActions: options.resolveCommandMenuActions,
|
|
114
115
|
executeCommandMenuAction: options.executeCommandMenuAction,
|
|
116
|
+
...(options.persistApiKey === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: {
|
|
119
|
+
persistApiKey: options.persistApiKey,
|
|
120
|
+
}),
|
|
115
121
|
});
|
|
116
122
|
const runtimeRailOptions: RuntimeRailInputOptions = {
|
|
117
123
|
workspace: options.workspace,
|
|
@@ -37,6 +37,7 @@ interface RuntimeModalInputOptions {
|
|
|
37
37
|
readonly scheduleConversationTitlePersist: () => void;
|
|
38
38
|
readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
|
|
39
39
|
readonly executeCommandMenuAction: (actionId: string) => void;
|
|
40
|
+
readonly persistApiKey?: (keyName: string, value: string) => void;
|
|
40
41
|
readonly markDirty: () => void;
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -97,6 +98,23 @@ export class RuntimeModalInput {
|
|
|
97
98
|
submitTaskEditorPayload: (payload) => {
|
|
98
99
|
options.taskEditorActions.submitTaskEditorPayload(payload);
|
|
99
100
|
},
|
|
101
|
+
...(options.persistApiKey === undefined
|
|
102
|
+
? {}
|
|
103
|
+
: {
|
|
104
|
+
getApiKeyPrompt: () => options.workspace.apiKeyPrompt,
|
|
105
|
+
setApiKeyPrompt: (
|
|
106
|
+
next: {
|
|
107
|
+
keyName: string;
|
|
108
|
+
displayName: string;
|
|
109
|
+
value: string;
|
|
110
|
+
error: string | null;
|
|
111
|
+
hasExistingValue: boolean;
|
|
112
|
+
} | null,
|
|
113
|
+
) => {
|
|
114
|
+
options.workspace.apiKeyPrompt = next;
|
|
115
|
+
},
|
|
116
|
+
persistApiKey: options.persistApiKey,
|
|
117
|
+
}),
|
|
100
118
|
getConversationTitleEdit: () => options.workspace.conversationTitleEdit,
|
|
101
119
|
getNewThreadPrompt: () => options.workspace.newThreadPrompt,
|
|
102
120
|
setNewThreadPrompt: (prompt) => {
|