@gajae-code/coding-agent 0.12.1 → 0.12.4
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/CHANGELOG.md +39 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
- package/dist/types/task/index.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-acp-conformance.ts +5 -0
- package/src/async/job-manager.ts +22 -20
- package/src/config/provider-ranking.ts +1 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +80 -10
- package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
- package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
- package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
- package/src/gjc-runtime/workflow-manifest.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/DESIGN.md +53 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/tool-status-header.ts +0 -1
- package/src/modes/controllers/command-controller.ts +38 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +186 -34
- package/src/modes/controllers/selector-controller.ts +115 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +1 -0
- package/src/session/agent-session.ts +22 -1
- package/src/session/internal/managed-session-scope.ts +158 -107
- package/src/session/internal/managed-session-storage.ts +445 -57
- package/src/session/session-storage.ts +29 -44
- package/src/task/index.ts +257 -31
- package/src/task/output-manager.ts +24 -14
- package/src/task/receipt.ts +11 -1
- package/src/tools/index.ts +2 -0
|
@@ -22,7 +22,12 @@ import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
|
22
22
|
import { HookInputComponent } from "../../modes/components/hook-input";
|
|
23
23
|
import { HookSelectorComponent } from "../../modes/components/hook-selector";
|
|
24
24
|
import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
|
|
25
|
-
import
|
|
25
|
+
import {
|
|
26
|
+
clearInteractiveActivityLoaders,
|
|
27
|
+
type InteractiveModeContext,
|
|
28
|
+
stopInteractiveActivityIndicator,
|
|
29
|
+
syncInteractiveActivityIndicator,
|
|
30
|
+
} from "../../modes/types";
|
|
26
31
|
import { createReadonlySessionManager } from "../../session/session-manager";
|
|
27
32
|
import { parseThinkingLevel } from "../../thinking";
|
|
28
33
|
import type { TodoPhase } from "../../tools/todo-write";
|
|
@@ -36,12 +41,55 @@ const HOOK_SELECTOR_CHROME_ROWS = 7;
|
|
|
36
41
|
const HOOK_SELECTOR_OUTLINE_ROWS = 2;
|
|
37
42
|
const HOOK_SELECTOR_INLINE_INPUT_ROWS = 2;
|
|
38
43
|
|
|
44
|
+
const EXTENSION_ACTION_MUTATIONS: ReadonlySet<PropertyKey> = new Set([
|
|
45
|
+
"sendMessage",
|
|
46
|
+
"sendUserMessage",
|
|
47
|
+
"appendEntry",
|
|
48
|
+
"setLabel",
|
|
49
|
+
"setActiveTools",
|
|
50
|
+
"setModel",
|
|
51
|
+
"setThinkingLevel",
|
|
52
|
+
"setThinkingVisibility",
|
|
53
|
+
"cycleThinkingLevel",
|
|
54
|
+
"setThinkingLevelForControl",
|
|
55
|
+
"setThinkingVisibilityForControl",
|
|
56
|
+
"setModelTemporaryForControl",
|
|
57
|
+
"setSessionName",
|
|
58
|
+
]);
|
|
59
|
+
const EXTENSION_CONTEXT_MUTATIONS: ReadonlySet<PropertyKey> = new Set([
|
|
60
|
+
"abort",
|
|
61
|
+
"abortPromptAndWait",
|
|
62
|
+
"shutdown",
|
|
63
|
+
"compact",
|
|
64
|
+
"clearContext",
|
|
65
|
+
"cycleModel",
|
|
66
|
+
"setModelProfile",
|
|
67
|
+
"cycleThinkingLevel",
|
|
68
|
+
"setQueueMode",
|
|
69
|
+
"invokeSkill",
|
|
70
|
+
"setPlanMode",
|
|
71
|
+
"operateGoal",
|
|
72
|
+
"setSdkPermissionProvider",
|
|
73
|
+
"setSdkClientBridge",
|
|
74
|
+
"sdkControl",
|
|
75
|
+
]);
|
|
76
|
+
const EXTENSION_COMMAND_MUTATIONS: ReadonlySet<PropertyKey> = new Set([
|
|
77
|
+
"reload",
|
|
78
|
+
"newSession",
|
|
79
|
+
"branch",
|
|
80
|
+
"navigateTree",
|
|
81
|
+
"compact",
|
|
82
|
+
"switchSession",
|
|
83
|
+
]);
|
|
84
|
+
|
|
39
85
|
export class ExtensionUiController {
|
|
40
86
|
#extensionTerminalInputUnsubscribers = new Set<() => void>();
|
|
87
|
+
#extensionErrorUnsubscribe?: () => void;
|
|
41
88
|
#hookWidgetsAbove = new Map<string, ExtensionUiComponent>();
|
|
42
89
|
#hookWidgetsBelow = new Map<string, ExtensionUiComponent>();
|
|
43
90
|
#activeHookCustomComponent?: Component & { dispose?(): void };
|
|
44
91
|
#activeHookCustomOverlay?: OverlayHandle;
|
|
92
|
+
#activeHookCustomCancel?: () => void;
|
|
45
93
|
|
|
46
94
|
constructor(private ctx: InteractiveModeContext) {}
|
|
47
95
|
|
|
@@ -317,6 +365,27 @@ export class ExtensionUiController {
|
|
|
317
365
|
this.ctx.restoreComposer();
|
|
318
366
|
}
|
|
319
367
|
|
|
368
|
+
#isStopped(): boolean {
|
|
369
|
+
return this.ctx.isStopped?.() === true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#assertActive(): void {
|
|
373
|
+
if (this.#isStopped()) throw Object.assign(new Error("Interactive mode stopped"), { code: "cancelled" });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#guardMutations<T extends object>(target: T, mutationNames: ReadonlySet<PropertyKey>): T {
|
|
377
|
+
return new Proxy(target, {
|
|
378
|
+
get: (current, property, receiver) => {
|
|
379
|
+
const member = Reflect.get(current, property, receiver) as unknown;
|
|
380
|
+
if (!mutationNames.has(property) || typeof member !== "function") return member;
|
|
381
|
+
return (...args: unknown[]) => {
|
|
382
|
+
this.#assertActive();
|
|
383
|
+
return Reflect.apply(member, current, args);
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
320
389
|
/**
|
|
321
390
|
* Initialize the hook system with TUI-based UI context.
|
|
322
391
|
*/
|
|
@@ -329,13 +398,19 @@ export class ExtensionUiController {
|
|
|
329
398
|
notify: (message, type) => this.showHookNotify(message, type),
|
|
330
399
|
onTerminalInput: handler => this.addExtensionTerminalInputListener(handler),
|
|
331
400
|
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
332
|
-
setWorkingMessage: message =>
|
|
401
|
+
setWorkingMessage: message => {
|
|
402
|
+
if (!this.#isStopped()) this.ctx.setWorkingMessage(message);
|
|
403
|
+
},
|
|
333
404
|
setWidget: (key, content, options) => this.setHookWidget(key, content, options),
|
|
334
|
-
setTitle: title =>
|
|
405
|
+
setTitle: title => {
|
|
406
|
+
if (!this.#isStopped()) setTerminalTitle(title);
|
|
407
|
+
},
|
|
335
408
|
custom: (factory, options) => this.showHookCustom(factory, options),
|
|
336
|
-
setEditorText: text =>
|
|
409
|
+
setEditorText: text => {
|
|
410
|
+
if (!this.#isStopped()) this.ctx.editor.setText(text);
|
|
411
|
+
},
|
|
337
412
|
pasteToEditor: text => {
|
|
338
|
-
this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
|
|
413
|
+
if (!this.#isStopped()) this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
|
|
339
414
|
},
|
|
340
415
|
getEditorText: () => this.ctx.editor.getText(),
|
|
341
416
|
editor: (title, prefill, dialogOptions, editorOptions) =>
|
|
@@ -346,17 +421,22 @@ export class ExtensionUiController {
|
|
|
346
421
|
getAllThemes: async () => (await getAvailableThemesWithPaths()).map(t => ({ name: t.name, path: t.path })),
|
|
347
422
|
getTheme: name => getThemeByName(name),
|
|
348
423
|
setTheme: async themeArg => {
|
|
424
|
+
if (this.#isStopped()) return { success: false, error: "Interactive mode stopped" };
|
|
349
425
|
if (typeof themeArg === "string") {
|
|
350
|
-
return await setTheme(themeArg, true);
|
|
426
|
+
return await setTheme(themeArg, true, { shouldApply: () => !this.#isStopped() });
|
|
351
427
|
}
|
|
352
428
|
// Theme object passed directly - not supported in current implementation
|
|
353
429
|
return Promise.resolve({ success: false, error: "Direct theme object not supported" });
|
|
354
430
|
},
|
|
355
431
|
setFooter: () => {},
|
|
356
432
|
setHeader: () => {},
|
|
357
|
-
setEditorComponent: factory =>
|
|
433
|
+
setEditorComponent: factory => {
|
|
434
|
+
if (!this.#isStopped()) this.ctx.setEditorComponent(factory);
|
|
435
|
+
},
|
|
358
436
|
getToolsExpanded: () => this.ctx.toolOutputExpanded,
|
|
359
|
-
setToolsExpanded: expanded =>
|
|
437
|
+
setToolsExpanded: expanded => {
|
|
438
|
+
if (!this.#isStopped()) this.ctx.setToolsExpanded(expanded);
|
|
439
|
+
},
|
|
360
440
|
};
|
|
361
441
|
this.ctx.setToolUIContext(uiContext, true);
|
|
362
442
|
|
|
@@ -370,8 +450,11 @@ export class ExtensionUiController {
|
|
|
370
450
|
const wasStreaming = this.ctx.session.isStreaming;
|
|
371
451
|
this.ctx.session
|
|
372
452
|
.sendCustomMessage(message, options)
|
|
373
|
-
.then(() =>
|
|
453
|
+
.then(() => {
|
|
454
|
+
if (!this.#isStopped()) this.#applyCustomMessageDisplay(wasStreaming, message.display);
|
|
455
|
+
})
|
|
374
456
|
.catch((err: unknown) => {
|
|
457
|
+
if (this.#isStopped()) return;
|
|
375
458
|
this.ctx.showError(
|
|
376
459
|
`Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
377
460
|
);
|
|
@@ -479,10 +562,12 @@ export class ExtensionUiController {
|
|
|
479
562
|
reload: async () => {
|
|
480
563
|
const previousSessionId = this.ctx.sessionManager.getSessionId();
|
|
481
564
|
await this.ctx.session.reload();
|
|
565
|
+
if (this.#isStopped()) return;
|
|
482
566
|
const sessionIdentityChanged = previousSessionId !== this.ctx.sessionManager.getSessionId();
|
|
483
567
|
if (sessionIdentityChanged) this.ctx.resetIrcSidebarSession();
|
|
484
568
|
this.ctx.rebuildInitialMessages(sessionIdentityChanged ? "replace-identity" : "reconcile-same-transcript");
|
|
485
569
|
await this.ctx.reloadTodos();
|
|
570
|
+
if (this.#isStopped()) return;
|
|
486
571
|
this.ctx.showStatus("Reloaded session");
|
|
487
572
|
},
|
|
488
573
|
newSession: async options => {
|
|
@@ -491,18 +576,17 @@ export class ExtensionUiController {
|
|
|
491
576
|
if (!success) {
|
|
492
577
|
return { cancelled: true };
|
|
493
578
|
}
|
|
579
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
580
|
+
clearInteractiveActivityLoaders(this.ctx);
|
|
494
581
|
cleanupPreviousSessionUi();
|
|
495
582
|
|
|
496
|
-
|
|
497
|
-
this.ctx.loadingAnimation.stop();
|
|
498
|
-
this.ctx.loadingAnimation = undefined;
|
|
499
|
-
}
|
|
500
|
-
this.ctx.statusContainer.clear();
|
|
583
|
+
stopInteractiveActivityIndicator(this.ctx);
|
|
501
584
|
this.ctx.resetIrcSidebarSession();
|
|
502
585
|
setSessionTerminalTitle(this.ctx.sessionManager.getSessionName(), this.ctx.sessionManager.getCwd());
|
|
503
586
|
|
|
504
587
|
if (options?.setup) {
|
|
505
588
|
await options.setup(this.ctx.sessionManager);
|
|
589
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
506
590
|
}
|
|
507
591
|
|
|
508
592
|
this.ctx.statusLine.invalidate();
|
|
@@ -522,12 +606,14 @@ export class ExtensionUiController {
|
|
|
522
606
|
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
|
|
523
607
|
);
|
|
524
608
|
await this.ctx.reloadTodos();
|
|
609
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
525
610
|
this.ctx.ui.requestRender();
|
|
526
611
|
|
|
527
612
|
return { cancelled: false };
|
|
528
613
|
},
|
|
529
614
|
branch: async entryId => {
|
|
530
615
|
const result = await this.ctx.session.branch(entryId);
|
|
616
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
531
617
|
if (result.cancelled) {
|
|
532
618
|
return { cancelled: true };
|
|
533
619
|
}
|
|
@@ -536,6 +622,7 @@ export class ExtensionUiController {
|
|
|
536
622
|
// Update UI
|
|
537
623
|
this.ctx.rebuildInitialMessages("replace-identity");
|
|
538
624
|
await this.ctx.reloadTodos();
|
|
625
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
539
626
|
this.ctx.editor.setText(result.selectedText);
|
|
540
627
|
this.ctx.showStatus("Branched to new session");
|
|
541
628
|
|
|
@@ -543,6 +630,7 @@ export class ExtensionUiController {
|
|
|
543
630
|
},
|
|
544
631
|
navigateTree: async (targetId, options) => {
|
|
545
632
|
const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
|
|
633
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
546
634
|
if (result.cancelled) {
|
|
547
635
|
return { cancelled: true };
|
|
548
636
|
}
|
|
@@ -550,6 +638,7 @@ export class ExtensionUiController {
|
|
|
550
638
|
// Update UI
|
|
551
639
|
this.ctx.rebuildInitialMessages("reconcile-same-transcript");
|
|
552
640
|
await this.ctx.reloadTodos();
|
|
641
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
553
642
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
554
643
|
this.ctx.editor.setText(result.editorText);
|
|
555
644
|
}
|
|
@@ -563,9 +652,11 @@ export class ExtensionUiController {
|
|
|
563
652
|
|
|
564
653
|
this.clearHookWidgets();
|
|
565
654
|
const result = await this.ctx.session.switchSession(sessionPath);
|
|
655
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
566
656
|
if (!result) {
|
|
567
657
|
return { cancelled: true };
|
|
568
658
|
}
|
|
659
|
+
clearInteractiveActivityLoaders(this.ctx);
|
|
569
660
|
const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
|
|
570
661
|
if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
|
|
571
662
|
|
|
@@ -574,15 +665,23 @@ export class ExtensionUiController {
|
|
|
574
665
|
switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
|
|
575
666
|
);
|
|
576
667
|
await this.ctx.reloadTodos();
|
|
668
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
669
|
+
syncInteractiveActivityIndicator(this.ctx);
|
|
577
670
|
return { cancelled: false };
|
|
578
671
|
},
|
|
579
672
|
};
|
|
580
673
|
|
|
581
|
-
extensionRunner.initialize(
|
|
674
|
+
extensionRunner.initialize(
|
|
675
|
+
this.#guardMutations(actions, EXTENSION_ACTION_MUTATIONS),
|
|
676
|
+
this.#guardMutations(contextActions, EXTENSION_CONTEXT_MUTATIONS),
|
|
677
|
+
this.#guardMutations(commandActions, EXTENSION_COMMAND_MUTATIONS),
|
|
678
|
+
uiContext,
|
|
679
|
+
);
|
|
582
680
|
|
|
583
681
|
// Subscribe to extension errors
|
|
584
|
-
|
|
585
|
-
|
|
682
|
+
this.#extensionErrorUnsubscribe?.();
|
|
683
|
+
this.#extensionErrorUnsubscribe = extensionRunner.onError((error: ExtensionError) => {
|
|
684
|
+
if (!this.#isStopped()) this.showExtensionError(error.extensionPath, error.error);
|
|
586
685
|
});
|
|
587
686
|
|
|
588
687
|
// Emit session_start event
|
|
@@ -592,6 +691,7 @@ export class ExtensionUiController {
|
|
|
592
691
|
}
|
|
593
692
|
|
|
594
693
|
setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
|
|
694
|
+
if (this.#isStopped()) return;
|
|
595
695
|
const placement = options?.placement ?? "aboveEditor";
|
|
596
696
|
this.#removeHookWidget(this.#hookWidgetsAbove, key);
|
|
597
697
|
this.#removeHookWidget(this.#hookWidgetsBelow, key);
|
|
@@ -781,10 +881,12 @@ export class ExtensionUiController {
|
|
|
781
881
|
}
|
|
782
882
|
const previousSessionId = this.ctx.sessionManager.getSessionId();
|
|
783
883
|
await this.ctx.session.reload();
|
|
884
|
+
if (this.#isStopped()) return;
|
|
784
885
|
const sessionIdentityChanged = previousSessionId !== this.ctx.sessionManager.getSessionId();
|
|
785
886
|
if (sessionIdentityChanged) this.ctx.resetIrcSidebarSession();
|
|
786
887
|
this.ctx.rebuildInitialMessages(sessionIdentityChanged ? "replace-identity" : "reconcile-same-transcript");
|
|
787
888
|
await this.ctx.reloadTodos();
|
|
889
|
+
if (this.#isStopped()) return;
|
|
788
890
|
this.ctx.showStatus("Reloaded session");
|
|
789
891
|
},
|
|
790
892
|
newSession: async options => {
|
|
@@ -796,17 +898,16 @@ export class ExtensionUiController {
|
|
|
796
898
|
if (!success) {
|
|
797
899
|
return { cancelled: true };
|
|
798
900
|
}
|
|
901
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
902
|
+
clearInteractiveActivityLoaders(this.ctx);
|
|
799
903
|
cleanupPreviousSessionUi();
|
|
800
904
|
|
|
801
|
-
|
|
802
|
-
this.ctx.loadingAnimation.stop();
|
|
803
|
-
this.ctx.loadingAnimation = undefined;
|
|
804
|
-
}
|
|
805
|
-
this.ctx.statusContainer.clear();
|
|
905
|
+
stopInteractiveActivityIndicator(this.ctx);
|
|
806
906
|
this.ctx.resetIrcSidebarSession();
|
|
807
907
|
|
|
808
908
|
if (options?.setup) {
|
|
809
909
|
await options.setup(this.ctx.sessionManager);
|
|
910
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
810
911
|
}
|
|
811
912
|
|
|
812
913
|
prepareTranscriptRebuild(this.ctx.ui, "replace-identity");
|
|
@@ -821,6 +922,7 @@ export class ExtensionUiController {
|
|
|
821
922
|
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 0),
|
|
822
923
|
);
|
|
823
924
|
await this.ctx.reloadTodos();
|
|
925
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
824
926
|
this.ctx.ui.requestRender();
|
|
825
927
|
|
|
826
928
|
return { cancelled: false };
|
|
@@ -830,6 +932,7 @@ export class ExtensionUiController {
|
|
|
830
932
|
return { cancelled: true };
|
|
831
933
|
}
|
|
832
934
|
const result = await this.ctx.session.branch(entryId);
|
|
935
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
833
936
|
if (result.cancelled) {
|
|
834
937
|
return { cancelled: true };
|
|
835
938
|
}
|
|
@@ -838,6 +941,7 @@ export class ExtensionUiController {
|
|
|
838
941
|
// Update UI
|
|
839
942
|
this.ctx.rebuildInitialMessages("replace-identity");
|
|
840
943
|
await this.ctx.reloadTodos();
|
|
944
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
841
945
|
this.ctx.editor.setText(result.selectedText);
|
|
842
946
|
this.ctx.showStatus("Branched to new session");
|
|
843
947
|
|
|
@@ -848,6 +952,7 @@ export class ExtensionUiController {
|
|
|
848
952
|
return { cancelled: true };
|
|
849
953
|
}
|
|
850
954
|
const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
|
|
955
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
851
956
|
if (result.cancelled) {
|
|
852
957
|
return { cancelled: true };
|
|
853
958
|
}
|
|
@@ -855,6 +960,7 @@ export class ExtensionUiController {
|
|
|
855
960
|
// Update UI
|
|
856
961
|
this.ctx.rebuildInitialMessages("reconcile-same-transcript");
|
|
857
962
|
await this.ctx.reloadTodos();
|
|
963
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
858
964
|
if (result.editorText && !this.ctx.editor.getText().trim()) {
|
|
859
965
|
this.ctx.editor.setText(result.editorText);
|
|
860
966
|
}
|
|
@@ -871,20 +977,29 @@ export class ExtensionUiController {
|
|
|
871
977
|
|
|
872
978
|
this.clearHookWidgets();
|
|
873
979
|
const result = await this.ctx.session.switchSession(sessionPath);
|
|
980
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
874
981
|
if (!result) {
|
|
875
982
|
return { cancelled: true };
|
|
876
983
|
}
|
|
984
|
+
clearInteractiveActivityLoaders(this.ctx);
|
|
877
985
|
const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
|
|
878
986
|
if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
|
|
879
987
|
this.ctx.rebuildInitialMessages(
|
|
880
988
|
switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
|
|
881
989
|
);
|
|
882
990
|
await this.ctx.reloadTodos();
|
|
991
|
+
if (this.#isStopped()) return { cancelled: true };
|
|
992
|
+
syncInteractiveActivityIndicator(this.ctx);
|
|
883
993
|
return { cancelled: false };
|
|
884
994
|
},
|
|
885
995
|
};
|
|
886
996
|
|
|
887
|
-
extensionRunner.initialize(
|
|
997
|
+
extensionRunner.initialize(
|
|
998
|
+
this.#guardMutations(actions, EXTENSION_ACTION_MUTATIONS),
|
|
999
|
+
this.#guardMutations(contextActions, EXTENSION_CONTEXT_MUTATIONS),
|
|
1000
|
+
this.#guardMutations(commandActions, EXTENSION_COMMAND_MUTATIONS),
|
|
1001
|
+
uiContext,
|
|
1002
|
+
);
|
|
888
1003
|
}
|
|
889
1004
|
|
|
890
1005
|
createBackgroundUiContext(): ExtensionUIContext {
|
|
@@ -1029,7 +1144,7 @@ export class ExtensionUiController {
|
|
|
1029
1144
|
* Set hook status text in the footer.
|
|
1030
1145
|
*/
|
|
1031
1146
|
setHookStatus(key: string, text: string | undefined): void {
|
|
1032
|
-
if (this.ctx.isBackgrounded) {
|
|
1147
|
+
if (this.#isStopped() || this.ctx.isBackgrounded) {
|
|
1033
1148
|
return;
|
|
1034
1149
|
}
|
|
1035
1150
|
this.ctx.statusLine.setHookStatus(key, text);
|
|
@@ -1044,6 +1159,7 @@ export class ExtensionUiController {
|
|
|
1044
1159
|
options: string[],
|
|
1045
1160
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
1046
1161
|
): Promise<string | undefined> {
|
|
1162
|
+
if (this.#isStopped()) return Promise.resolve(undefined);
|
|
1047
1163
|
const { promise, finish, attachAbort } = this.#createHookDialogState(
|
|
1048
1164
|
() => this.hideHookSelector(),
|
|
1049
1165
|
dialogOptions?.signal,
|
|
@@ -1148,8 +1264,9 @@ export class ExtensionUiController {
|
|
|
1148
1264
|
*/
|
|
1149
1265
|
hideHookSelector(): void {
|
|
1150
1266
|
this.ctx.hookSelector?.dispose();
|
|
1151
|
-
this.#restoreComposerEditor();
|
|
1152
1267
|
this.ctx.hookSelector = undefined;
|
|
1268
|
+
if (this.#isStopped()) return;
|
|
1269
|
+
this.#restoreComposerEditor();
|
|
1153
1270
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
1154
1271
|
this.ctx.ui.requestRender();
|
|
1155
1272
|
}
|
|
@@ -1171,6 +1288,7 @@ export class ExtensionUiController {
|
|
|
1171
1288
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
1172
1289
|
inputOptions?: { readonly initialValue?: string },
|
|
1173
1290
|
): Promise<string | undefined> {
|
|
1291
|
+
if (this.#isStopped()) return Promise.resolve(undefined);
|
|
1174
1292
|
const { promise, finish, attachAbort } = this.#createHookDialogState(
|
|
1175
1293
|
() => this.hideHookInput(),
|
|
1176
1294
|
dialogOptions?.signal,
|
|
@@ -1209,8 +1327,9 @@ export class ExtensionUiController {
|
|
|
1209
1327
|
*/
|
|
1210
1328
|
hideHookInput(): void {
|
|
1211
1329
|
this.ctx.hookInput?.dispose();
|
|
1212
|
-
this.#restoreComposerEditor();
|
|
1213
1330
|
this.ctx.hookInput = undefined;
|
|
1331
|
+
if (this.#isStopped()) return;
|
|
1332
|
+
this.#restoreComposerEditor();
|
|
1214
1333
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
1215
1334
|
this.ctx.ui.requestRender();
|
|
1216
1335
|
}
|
|
@@ -1224,6 +1343,7 @@ export class ExtensionUiController {
|
|
|
1224
1343
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
1225
1344
|
editorOptions?: { promptStyle?: boolean },
|
|
1226
1345
|
): Promise<string | undefined> {
|
|
1346
|
+
if (this.#isStopped()) return Promise.resolve(undefined);
|
|
1227
1347
|
const { promise, finish, attachAbort } = this.#createHookDialogState(
|
|
1228
1348
|
() => this.hideHookEditor(),
|
|
1229
1349
|
dialogOptions?.signal,
|
|
@@ -1258,6 +1378,11 @@ export class ExtensionUiController {
|
|
|
1258
1378
|
* Hide the hook editor.
|
|
1259
1379
|
*/
|
|
1260
1380
|
hideHookEditor(): void {
|
|
1381
|
+
if (this.#isStopped()) {
|
|
1382
|
+
this.ctx.hookEditor?.dispose();
|
|
1383
|
+
this.ctx.hookEditor = undefined;
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1261
1386
|
this.#restoreComposerEditor();
|
|
1262
1387
|
this.ctx.hookEditor = undefined;
|
|
1263
1388
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
@@ -1268,6 +1393,7 @@ export class ExtensionUiController {
|
|
|
1268
1393
|
* Show a notification for hooks.
|
|
1269
1394
|
*/
|
|
1270
1395
|
showHookNotify(message: string, type?: "info" | "warning" | "error"): void {
|
|
1396
|
+
if (this.#isStopped()) return;
|
|
1271
1397
|
if (type === "error") {
|
|
1272
1398
|
this.ctx.showError(message);
|
|
1273
1399
|
} else if (type === "warning") {
|
|
@@ -1289,6 +1415,7 @@ export class ExtensionUiController {
|
|
|
1289
1415
|
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
|
|
1290
1416
|
options?: { overlay?: boolean },
|
|
1291
1417
|
): Promise<T> {
|
|
1418
|
+
if (this.#isStopped()) return undefined as T;
|
|
1292
1419
|
const savedText = this.ctx.editor.getText();
|
|
1293
1420
|
const keybindings = KeybindingsManager.inMemory();
|
|
1294
1421
|
|
|
@@ -1299,7 +1426,12 @@ export class ExtensionUiController {
|
|
|
1299
1426
|
const close = (result: T) => {
|
|
1300
1427
|
if (closed) return;
|
|
1301
1428
|
closed = true;
|
|
1429
|
+
this.#activeHookCustomCancel = undefined;
|
|
1302
1430
|
this.#clearActiveHookCustom();
|
|
1431
|
+
if (this.#isStopped()) {
|
|
1432
|
+
resolve(result);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1303
1435
|
if (!options?.overlay) {
|
|
1304
1436
|
this.#restoreComposerEditor();
|
|
1305
1437
|
this.ctx.editor.setText(savedText);
|
|
@@ -1309,10 +1441,16 @@ export class ExtensionUiController {
|
|
|
1309
1441
|
resolve(result);
|
|
1310
1442
|
};
|
|
1311
1443
|
|
|
1444
|
+
this.#activeHookCustomCancel?.();
|
|
1312
1445
|
this.#clearActiveHookCustom();
|
|
1446
|
+
this.#activeHookCustomCancel = () => close(undefined as T);
|
|
1313
1447
|
Promise.try(() => factory(this.ctx.ui, theme, keybindings, close)).then(c => {
|
|
1314
|
-
if (closed) {
|
|
1448
|
+
if (closed || this.#isStopped()) {
|
|
1315
1449
|
c.dispose?.();
|
|
1450
|
+
if (!closed) {
|
|
1451
|
+
closed = true;
|
|
1452
|
+
resolve(undefined as T);
|
|
1453
|
+
}
|
|
1316
1454
|
return;
|
|
1317
1455
|
}
|
|
1318
1456
|
component = c;
|
|
@@ -1341,6 +1479,7 @@ export class ExtensionUiController {
|
|
|
1341
1479
|
* Show an extension error in the UI.
|
|
1342
1480
|
*/
|
|
1343
1481
|
addExtensionTerminalInputListener(handler: TerminalInputHandler): () => void {
|
|
1482
|
+
if (this.#isStopped()) return () => {};
|
|
1344
1483
|
const unsubscribe = this.ctx.ui.addInputListener(handler);
|
|
1345
1484
|
this.#extensionTerminalInputUnsubscribers.add(unsubscribe);
|
|
1346
1485
|
return () => {
|
|
@@ -1369,7 +1508,17 @@ export class ExtensionUiController {
|
|
|
1369
1508
|
this.#extensionTerminalInputUnsubscribers.clear();
|
|
1370
1509
|
}
|
|
1371
1510
|
|
|
1511
|
+
dispose(): void {
|
|
1512
|
+
this.#extensionErrorUnsubscribe?.();
|
|
1513
|
+
this.#extensionErrorUnsubscribe = undefined;
|
|
1514
|
+
this.#activeHookCustomCancel?.();
|
|
1515
|
+
this.#activeHookCustomCancel = undefined;
|
|
1516
|
+
this.clearExtensionTerminalInputListeners();
|
|
1517
|
+
this.clearHookWidgets();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1372
1520
|
showExtensionError(extensionPath: string, error: string): void {
|
|
1521
|
+
if (this.#isStopped()) return;
|
|
1373
1522
|
const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
|
|
1374
1523
|
this.ctx.chatContainer.addChild(errorText);
|
|
1375
1524
|
this.ctx.ui.requestRender();
|
|
@@ -1395,6 +1544,7 @@ export class ExtensionUiController {
|
|
|
1395
1544
|
}
|
|
1396
1545
|
|
|
1397
1546
|
#sendExtensionUserMessage: SendUserMessageHandler = (content, options) => {
|
|
1547
|
+
if (this.#isStopped()) return Promise.resolve();
|
|
1398
1548
|
// Compute queued BEFORE send: prompt() may flip session.isStreaming synchronously.
|
|
1399
1549
|
const queued = Boolean(options?.deliverAs) || this.ctx.session.isStreaming;
|
|
1400
1550
|
// Call send first so the busy/queued path finds the session queue populated
|
|
@@ -1402,6 +1552,7 @@ export class ExtensionUiController {
|
|
|
1402
1552
|
const send = this.ctx.session.sendUserMessage(content, options);
|
|
1403
1553
|
applyInjectedUserSubmission(this.ctx, { content, queued });
|
|
1404
1554
|
void send.catch((err: unknown) => {
|
|
1555
|
+
if (this.#isStopped()) return;
|
|
1405
1556
|
this.ctx.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1406
1557
|
});
|
|
1407
1558
|
return send;
|
|
@@ -1425,20 +1576,21 @@ export class ExtensionUiController {
|
|
|
1425
1576
|
} {
|
|
1426
1577
|
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
1427
1578
|
let settled = false;
|
|
1428
|
-
|
|
1429
|
-
hide();
|
|
1430
|
-
if (!settled) {
|
|
1431
|
-
settled = true;
|
|
1432
|
-
resolve(undefined);
|
|
1433
|
-
}
|
|
1434
|
-
};
|
|
1579
|
+
let unregisterStop: (() => void) | undefined;
|
|
1435
1580
|
const finish = (value: string | undefined) => {
|
|
1436
1581
|
if (settled) return;
|
|
1437
1582
|
settled = true;
|
|
1438
1583
|
signal?.removeEventListener("abort", onAbort);
|
|
1584
|
+
unregisterStop?.();
|
|
1439
1585
|
resolve(value);
|
|
1440
1586
|
};
|
|
1587
|
+
const onAbort = () => {
|
|
1588
|
+
hide();
|
|
1589
|
+
finish(undefined);
|
|
1590
|
+
};
|
|
1441
1591
|
const attachAbort = () => {
|
|
1592
|
+
unregisterStop = this.ctx.onStop?.(onAbort);
|
|
1593
|
+
if (settled) unregisterStop?.();
|
|
1442
1594
|
if (!signal) return;
|
|
1443
1595
|
if (signal.aborted) {
|
|
1444
1596
|
onAbort();
|