@gajae-code/coding-agent 0.12.10 → 0.12.12
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 +47 -2
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +13 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +25 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +12 -2
- package/dist/types/session/session-manager.d.ts +40 -5
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/todo-write.d.ts +2 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/update-cli.ts +90 -23
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +18 -0
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +31 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +13 -12
- package/src/main.ts +203 -67
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/utils/host-status.ts +38 -0
- package/src/prompts/tools/computer.md +6 -1
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +18 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/index.ts +70 -0
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +25 -7
- package/src/sdk/bus/telegram-daemon.ts +1961 -497
- package/src/sdk/bus/telegram-reference.ts +42 -6
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +139 -28
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +331 -30
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +440 -243
- package/src/session/session-manager.ts +288 -44
- package/src/session/session-storage.ts +3 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/computer.ts +322 -45
- package/src/tools/index.ts +8 -0
- package/src/tools/todo-write.ts +29 -11
- package/src/utils/npm-registry.ts +660 -0
package/src/main.ts
CHANGED
|
@@ -76,20 +76,13 @@ import { persistTaskTokenLog, resolveTaskTokenLogDir, taskTokenLogFromUsage } fr
|
|
|
76
76
|
import type { LspStartupServerInfo } from "./tools";
|
|
77
77
|
import { getDisplayChangelogEntries, getInstalledVersionChangelogEntry, getNewEntries } from "./utils/changelog";
|
|
78
78
|
import type { EventBus } from "./utils/event-bus";
|
|
79
|
+
import { fetchLatestPackageVersion } from "./utils/npm-registry";
|
|
79
80
|
|
|
80
81
|
async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
|
|
81
82
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const data = (await response.json()) as { version?: string };
|
|
86
|
-
const latestVersion = data.version;
|
|
87
|
-
|
|
88
|
-
if (latestVersion && Bun.semver.order(latestVersion, currentVersion) > 0) {
|
|
89
|
-
return latestVersion;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return undefined;
|
|
83
|
+
// Resolved from npm config so mirrored/firewalled networks are checked too.
|
|
84
|
+
const { version } = await fetchLatestPackageVersion("@gajae-code/coding-agent");
|
|
85
|
+
return Bun.semver.order(version, currentVersion) > 0 ? version : undefined;
|
|
93
86
|
} catch {
|
|
94
87
|
return undefined;
|
|
95
88
|
}
|
|
@@ -406,6 +399,7 @@ type StartupModelProfileArgs = {
|
|
|
406
399
|
parsedArgs: Pick<Args, "default" | "model" | "mpreset" | "thinking">;
|
|
407
400
|
startupModel?: CreateAgentSessionOptions["model"];
|
|
408
401
|
startupThinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
|
|
402
|
+
preferCachedModels?: boolean;
|
|
409
403
|
};
|
|
410
404
|
|
|
411
405
|
async function applyStartupModelProfilesWithPolicy(
|
|
@@ -416,16 +410,17 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
416
410
|
profileName: string,
|
|
417
411
|
persistDefault: boolean,
|
|
418
412
|
options: { thinkingLevelOverride?: CreateAgentSessionOptions["thinkingLevel"] } = {},
|
|
419
|
-
): Promise<
|
|
413
|
+
): Promise<boolean> => {
|
|
420
414
|
try {
|
|
421
415
|
await activateModelProfile(
|
|
422
416
|
{ session: args.session, modelRegistry: args.modelRegistry, settings: args.settings, profileName },
|
|
423
417
|
{ persistDefault, thinkingLevelOverride: options.thinkingLevelOverride },
|
|
424
418
|
);
|
|
419
|
+
return true;
|
|
425
420
|
} catch (error) {
|
|
426
421
|
if (onCredentialError && error instanceof ModelProfileCredentialError) {
|
|
427
422
|
onCredentialError(error);
|
|
428
|
-
return;
|
|
423
|
+
return false;
|
|
429
424
|
}
|
|
430
425
|
throw error;
|
|
431
426
|
}
|
|
@@ -436,19 +431,40 @@ async function applyStartupModelProfilesWithPolicy(
|
|
|
436
431
|
// deferred `--model <pattern>` path resolved inside createAgentSession.
|
|
437
432
|
const explicitModel = args.parsedArgs.model ? (args.startupModel ?? args.session.model) : undefined;
|
|
438
433
|
const defaultProfile = args.settings.get("modelProfile.default");
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
434
|
+
const preferCachedProfiles = args.preferCachedModels === true && args.parsedArgs.mpreset !== undefined;
|
|
435
|
+
const applyConfiguredProfiles = async (): Promise<boolean> => {
|
|
436
|
+
let applied = true;
|
|
437
|
+
if (defaultProfile) {
|
|
438
|
+
applied =
|
|
439
|
+
(await applyProfile(defaultProfile, false, {
|
|
440
|
+
thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
|
|
441
|
+
? args.settings.get("defaultThinkingLevel")
|
|
442
|
+
: undefined,
|
|
443
|
+
})) && applied;
|
|
444
|
+
}
|
|
445
|
+
if (args.parsedArgs.mpreset) {
|
|
446
|
+
applied = (await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true)) && applied;
|
|
447
|
+
}
|
|
448
|
+
return applied;
|
|
449
|
+
};
|
|
442
450
|
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
451
|
+
if (preferCachedProfiles) {
|
|
452
|
+
let applied: boolean;
|
|
453
|
+
let refreshedOnline = false;
|
|
454
|
+
try {
|
|
455
|
+
applied = await applyConfiguredProfiles();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (error instanceof ModelProfileCredentialError) throw error;
|
|
458
|
+
await args.modelRegistry.refresh("online-if-uncached");
|
|
459
|
+
refreshedOnline = true;
|
|
460
|
+
applied = await applyConfiguredProfiles();
|
|
461
|
+
}
|
|
462
|
+
if (applied && !refreshedOnline) args.modelRegistry.refreshInBackground();
|
|
463
|
+
} else {
|
|
464
|
+
if (defaultProfile || args.parsedArgs.mpreset) {
|
|
465
|
+
await args.modelRegistry.refresh("online-if-uncached");
|
|
466
|
+
}
|
|
467
|
+
await applyConfiguredProfiles();
|
|
452
468
|
}
|
|
453
469
|
|
|
454
470
|
// Explicit CLI --model/--thinking must win over any activated or skipped profile.
|
|
@@ -509,20 +525,52 @@ export async function applyStartupModelProfilesForRoot(
|
|
|
509
525
|
resumeAction: "continue-tail" | "open-idle" | undefined;
|
|
510
526
|
},
|
|
511
527
|
): Promise<{ recoverableErrors: string[] }> {
|
|
528
|
+
const policyArgs = { ...args, preferCachedModels: args.isInteractive };
|
|
512
529
|
if (!isStartupModelProfileCredentialRecoveryEligible(args)) {
|
|
513
|
-
|
|
530
|
+
try {
|
|
531
|
+
await applyStartupModelProfilesWithPolicy(policyArgs);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
await exitForStartupModelProfileError(args, error);
|
|
534
|
+
}
|
|
514
535
|
return { recoverableErrors: [] };
|
|
515
536
|
}
|
|
516
537
|
|
|
517
538
|
const recoverableErrors: string[] = [];
|
|
518
539
|
try {
|
|
519
|
-
await applyStartupModelProfilesWithPolicy(
|
|
540
|
+
await applyStartupModelProfilesWithPolicy(policyArgs, error => recoverableErrors.push(error.message));
|
|
520
541
|
} catch (error) {
|
|
521
542
|
await exitForStartupModelProfileError(args, error);
|
|
522
543
|
}
|
|
523
544
|
return { recoverableErrors };
|
|
524
545
|
}
|
|
525
546
|
|
|
547
|
+
type DeferredModelProfileStartup = () => Promise<{ recoverableErrors: string[] }>;
|
|
548
|
+
class DeferredModelProfileStartupError extends Error {
|
|
549
|
+
readonly profileError: unknown;
|
|
550
|
+
|
|
551
|
+
constructor(profileError: unknown) {
|
|
552
|
+
super("Deferred model profile startup failed");
|
|
553
|
+
this.profileError = profileError;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function applyDeferredStartupModelProfilesForRoot(
|
|
558
|
+
args: StartupModelProfileArgs & {
|
|
559
|
+
isInteractive: boolean;
|
|
560
|
+
hasInteractiveTerminal: boolean;
|
|
561
|
+
initialMessage: string | undefined;
|
|
562
|
+
initialMessages: readonly string[];
|
|
563
|
+
resumeAction: "continue-tail" | "open-idle" | undefined;
|
|
564
|
+
},
|
|
565
|
+
): Promise<{ recoverableErrors: string[] }> {
|
|
566
|
+
const recoverableErrors: string[] = [];
|
|
567
|
+
const onCredentialError = isStartupModelProfileCredentialRecoveryEligible(args)
|
|
568
|
+
? (error: ModelProfileCredentialError) => recoverableErrors.push(error.message)
|
|
569
|
+
: undefined;
|
|
570
|
+
await applyStartupModelProfilesWithPolicy({ ...args, preferCachedModels: true }, onCredentialError);
|
|
571
|
+
return { recoverableErrors };
|
|
572
|
+
}
|
|
573
|
+
|
|
526
574
|
interface InteractiveModeFactoryOptions {
|
|
527
575
|
session: AgentSession;
|
|
528
576
|
version: string;
|
|
@@ -616,6 +664,8 @@ export async function runInteractiveMode(
|
|
|
616
664
|
initialImages?: ImageContent[],
|
|
617
665
|
createInteractiveMode?: CreateInteractiveMode,
|
|
618
666
|
resumeAction?: "continue-tail" | "open-idle",
|
|
667
|
+
startDeferredMcpConfig?: CreateAgentSessionResult["startDeferredMcpConfig"],
|
|
668
|
+
startDeferredModelProfiles?: DeferredModelProfileStartup,
|
|
619
669
|
): Promise<void> {
|
|
620
670
|
const mode = createInteractiveMode
|
|
621
671
|
? createInteractiveMode({
|
|
@@ -659,45 +709,98 @@ export async function runInteractiveMode(
|
|
|
659
709
|
mode.showStatus(notify.message);
|
|
660
710
|
}
|
|
661
711
|
}
|
|
712
|
+
if (startDeferredMcpConfig) {
|
|
713
|
+
mode.showStatus("Connecting MCP tools…");
|
|
714
|
+
void startDeferredMcpConfig()
|
|
715
|
+
.then(result => {
|
|
716
|
+
if (result.hasErrors) {
|
|
717
|
+
mode.showWarning(
|
|
718
|
+
result.loadedToolCount > 0 ? "Some MCP tools could not be loaded." : "MCP tools could not be loaded.",
|
|
719
|
+
);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
mode.showStatus(`${result.loadedToolCount} MCP tool${result.loadedToolCount === 1 ? "" : "s"} connected`);
|
|
723
|
+
})
|
|
724
|
+
.catch(() => {
|
|
725
|
+
logger.warn("Deferred MCP startup failed.");
|
|
726
|
+
mode.showError("MCP tools could not be loaded.");
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
let deferredModelProfileFailure: Promise<never> | undefined;
|
|
730
|
+
if (startDeferredModelProfiles) {
|
|
731
|
+
mode.showStatus("Loading model profile…");
|
|
732
|
+
deferredModelProfileFailure = startDeferredModelProfiles().then(
|
|
733
|
+
result => {
|
|
734
|
+
for (const error of result.recoverableErrors) mode.showError(error);
|
|
735
|
+
mode.showStatus("Model profile ready");
|
|
736
|
+
return Promise.withResolvers<never>().promise;
|
|
737
|
+
},
|
|
738
|
+
error => {
|
|
739
|
+
const message = error instanceof Error ? error.message : "Model profile could not be loaded.";
|
|
740
|
+
mode.showError(message);
|
|
741
|
+
throw new DeferredModelProfileStartupError(error);
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
}
|
|
662
745
|
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
746
|
+
const runStartupInputAndPromptLoop = async (): Promise<never> => {
|
|
747
|
+
const hasStartupInput = initialMessage !== undefined || initialMessages.length > 0;
|
|
748
|
+
if (!hasStartupInput && resumeAction === "continue-tail") {
|
|
749
|
+
try {
|
|
750
|
+
await session.continuePersistedHistory();
|
|
751
|
+
} catch (error: unknown) {
|
|
752
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
753
|
+
mode.showError(errorMessage);
|
|
754
|
+
}
|
|
670
755
|
}
|
|
671
|
-
}
|
|
672
756
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
757
|
+
if (initialMessage !== undefined) {
|
|
758
|
+
try {
|
|
759
|
+
await session.prompt(initialMessage, { images: initialImages });
|
|
760
|
+
} catch (error: unknown) {
|
|
761
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
762
|
+
mode.showError(errorMessage);
|
|
763
|
+
}
|
|
679
764
|
}
|
|
680
|
-
}
|
|
681
765
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
766
|
+
for (const message of initialMessages) {
|
|
767
|
+
try {
|
|
768
|
+
let text = message;
|
|
769
|
+
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
770
|
+
ctx: mode,
|
|
771
|
+
handleBackgroundCommand: () => mode.handleBackgroundCommand(),
|
|
772
|
+
});
|
|
773
|
+
if (slashResult === true) continue;
|
|
774
|
+
if (typeof slashResult === "string") text = slashResult;
|
|
775
|
+
await session.prompt(text);
|
|
776
|
+
} catch (error: unknown) {
|
|
777
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
|
778
|
+
mode.showError(errorMessage);
|
|
779
|
+
}
|
|
695
780
|
}
|
|
696
|
-
}
|
|
697
781
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
782
|
+
while (true) {
|
|
783
|
+
const input = await mode.getUserInput();
|
|
784
|
+
await submitInteractiveInput(mode, session, input);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
const inputLoop = runStartupInputAndPromptLoop();
|
|
789
|
+
if (!deferredModelProfileFailure) {
|
|
790
|
+
await inputLoop;
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
await Promise.race([inputLoop, deferredModelProfileFailure]);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
if (!(error instanceof DeferredModelProfileStartupError)) throw error;
|
|
797
|
+
try {
|
|
798
|
+
await mode.shutdown();
|
|
799
|
+
} finally {
|
|
800
|
+
mode.stop();
|
|
801
|
+
}
|
|
802
|
+
await inputLoop.catch(() => {});
|
|
803
|
+
throw error.profileError;
|
|
701
804
|
}
|
|
702
805
|
}
|
|
703
806
|
|
|
@@ -1494,7 +1597,13 @@ export async function runRootCommand(
|
|
|
1494
1597
|
sessionOptions.notificationHostModeSupported = isInteractive;
|
|
1495
1598
|
sessionOptions.sdkHostModeSupported = isInteractive;
|
|
1496
1599
|
sessionOptions.settings = settingsInstance;
|
|
1600
|
+
if (isInteractive && sessionOptions.mcpConfigPath) {
|
|
1601
|
+
sessionOptions.deferMcpConfigStartup = true;
|
|
1602
|
+
}
|
|
1497
1603
|
const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
|
|
1604
|
+
const deferMemoryBackendStartup =
|
|
1605
|
+
hasRootStartupProfile && !(parsedArgs.authBootstrap === true && isInteractive) && mode !== "acp";
|
|
1606
|
+
sessionOptions.deferMemoryBackendStartup = deferMemoryBackendStartup;
|
|
1498
1607
|
|
|
1499
1608
|
// Research-mode (RLM) preset: augment session options before session creation.
|
|
1500
1609
|
deps.rlmPreset?.applyOptions(sessionOptions, settingsInstance);
|
|
@@ -1574,10 +1683,16 @@ export async function runRootCommand(
|
|
|
1574
1683
|
if (!deps.suppressProcessExit) await postmortem.cleanup();
|
|
1575
1684
|
}
|
|
1576
1685
|
} else {
|
|
1577
|
-
const {
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1686
|
+
const {
|
|
1687
|
+
session,
|
|
1688
|
+
setToolUIContext,
|
|
1689
|
+
modelFallbackMessage,
|
|
1690
|
+
lspServers,
|
|
1691
|
+
mcpManager,
|
|
1692
|
+
startDeferredMcpConfig,
|
|
1693
|
+
startDeferredMemoryBackend,
|
|
1694
|
+
eventBus,
|
|
1695
|
+
} = await createSession(sessionOptions, { skipPostCreateModelRefresh: hasRootStartupProfile });
|
|
1581
1696
|
applyCliRuntimeApiKeyOverride(authStorage, parsedArgs.apiKey, session.model);
|
|
1582
1697
|
|
|
1583
1698
|
// Research-mode (RLM) preset: hard tool-boundary assertion after the registry is assembled.
|
|
@@ -1594,8 +1709,9 @@ export async function runRootCommand(
|
|
|
1594
1709
|
}
|
|
1595
1710
|
}
|
|
1596
1711
|
|
|
1712
|
+
let startDeferredModelProfiles: DeferredModelProfileStartup | undefined;
|
|
1597
1713
|
if (!(parsedArgs.authBootstrap === true && isInteractive)) {
|
|
1598
|
-
const
|
|
1714
|
+
const profileArgs = {
|
|
1599
1715
|
session,
|
|
1600
1716
|
settings: settingsInstance,
|
|
1601
1717
|
modelRegistry,
|
|
@@ -1607,9 +1723,27 @@ export async function runRootCommand(
|
|
|
1607
1723
|
initialMessage,
|
|
1608
1724
|
initialMessages: parsedArgs.messages,
|
|
1609
1725
|
resumeAction: bareResumeAction,
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1726
|
+
};
|
|
1727
|
+
if (isInteractive && parsedArgs.mpreset) {
|
|
1728
|
+
const ready = Promise.withResolvers<void>();
|
|
1729
|
+
session.extendStartupTurnBarrier(ready.promise);
|
|
1730
|
+
startDeferredModelProfiles = async () => {
|
|
1731
|
+
try {
|
|
1732
|
+
const result = await applyDeferredStartupModelProfilesForRoot(profileArgs);
|
|
1733
|
+
startDeferredMemoryBackend?.();
|
|
1734
|
+
ready.resolve();
|
|
1735
|
+
return result;
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
ready.reject(error);
|
|
1738
|
+
throw error;
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
} else {
|
|
1742
|
+
const { recoverableErrors } = await applyStartupModelProfilesForRoot(profileArgs);
|
|
1743
|
+
startDeferredMemoryBackend?.();
|
|
1744
|
+
for (const recoverableError of recoverableErrors) {
|
|
1745
|
+
notifs.push({ kind: "error", message: recoverableError });
|
|
1746
|
+
}
|
|
1613
1747
|
}
|
|
1614
1748
|
}
|
|
1615
1749
|
|
|
@@ -1690,6 +1824,8 @@ export async function runRootCommand(
|
|
|
1690
1824
|
initialImages,
|
|
1691
1825
|
deps.createInteractiveMode,
|
|
1692
1826
|
bareResumeAction,
|
|
1827
|
+
startDeferredMcpConfig,
|
|
1828
|
+
startDeferredModelProfiles,
|
|
1693
1829
|
);
|
|
1694
1830
|
}
|
|
1695
1831
|
} catch (error) {
|
package/src/modes/DESIGN.md
CHANGED
|
@@ -63,9 +63,11 @@ visible-width-aligned column capped at 30 cells, two spaces, then a truncated
|
|
|
63
63
|
value. The selected row uses the themed cursor; unselected rows reserve two
|
|
64
64
|
spaces. It centers the selected item inside its `maxVisible` window, reports
|
|
65
65
|
scroll position as `(current/total)`, places a blank row before the selected
|
|
66
|
-
item description, and ends with
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
item description, and ends with a dim keyboard hint. Printable text starts a
|
|
67
|
+
case-insensitive label search shown above the list; Backspace removes one
|
|
68
|
+
grapheme, and Escape clears a non-empty search before a later Escape cancels
|
|
69
|
+
the list. Hosts that need stable height reserve fixed description rows; the
|
|
70
|
+
status-line custom editor reserves two.
|
|
69
71
|
|
|
70
72
|
Submenus are a content replacement, not a modal overlay: a bold accent title,
|
|
71
73
|
optional muted description, optional preview, a spacer, a select/list control,
|
|
@@ -82,8 +84,9 @@ empty space and list density for an operational setup flow.
|
|
|
82
84
|
### Focus, cursor, keyboard, and input behavior
|
|
83
85
|
|
|
84
86
|
- Up/Down wrap within selector lists. Enter and Space activate the current
|
|
85
|
-
action.
|
|
86
|
-
|
|
87
|
+
action. Printable text filters settings by label. Escape first clears an
|
|
88
|
+
active filter, then follows the current component's cancel path before the
|
|
89
|
+
parent is allowed to close.
|
|
87
90
|
- The parent routes Tab/Left/Right to the tab bar except while a text input is
|
|
88
91
|
active. Text entry owns arrow keys and Tab in that state.
|
|
89
92
|
- `Input` has a visible `> ` prompt, a zero-width hardware cursor marker only
|
|
@@ -116,24 +116,16 @@ export class FooterComponent implements Component {
|
|
|
116
116
|
render(width: number): string[] {
|
|
117
117
|
const state = this.session.state;
|
|
118
118
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
totalInput += entry.message.usage.input;
|
|
130
|
-
totalOutput += entry.message.usage.output;
|
|
131
|
-
totalCacheRead += entry.message.usage.cacheRead;
|
|
132
|
-
totalCacheWrite += entry.message.usage.cacheWrite;
|
|
133
|
-
totalCost += entry.message.usage.cost.total;
|
|
134
|
-
totalPremiumRequests += entry.message.usage.premiumRequests ?? 0;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
119
|
+
// Use the session manager's cumulative index so parent and task usage are
|
|
120
|
+
// accounted for consistently without rescanning persisted entries here.
|
|
121
|
+
const {
|
|
122
|
+
input: totalInput,
|
|
123
|
+
output: totalOutput,
|
|
124
|
+
cacheRead: totalCacheRead,
|
|
125
|
+
cacheWrite: totalCacheWrite,
|
|
126
|
+
cost: totalCost,
|
|
127
|
+
premiumRequests: totalPremiumRequests,
|
|
128
|
+
} = this.session.sessionManager.getUsageStatistics();
|
|
137
129
|
|
|
138
130
|
// Calculate context usage from session (handles compaction correctly).
|
|
139
131
|
// After compaction, tokens are unknown until the next LLM response.
|
|
@@ -197,7 +197,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
197
197
|
|
|
198
198
|
constructor(
|
|
199
199
|
sessions: SessionInfo[],
|
|
200
|
-
private readonly onSelect: (sessionPath: string) => void
|
|
200
|
+
private readonly onSelect: (sessionPath: string) => void | Promise<void>,
|
|
201
201
|
private readonly onCancel: () => void,
|
|
202
202
|
private readonly onExit: () => void,
|
|
203
203
|
private readonly onDelete?: (session: SessionInfo) => Promise<boolean>,
|
|
@@ -214,7 +214,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
214
214
|
this.#sessionList = new SessionList(sessions);
|
|
215
215
|
this.#sessionList.onSelect = session => {
|
|
216
216
|
if (this.inspector) void this.#inspect(session);
|
|
217
|
-
else this
|
|
217
|
+
else this.#dispatchSelect(session.path);
|
|
218
218
|
};
|
|
219
219
|
this.#sessionList.onCancel = () => this.#cancel();
|
|
220
220
|
this.#sessionList.onExit = () => this.#exit();
|
|
@@ -241,6 +241,29 @@ export class SessionSelectorComponent extends Container {
|
|
|
241
241
|
this.#onRequestRender?.();
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Observe async `onSelect` handlers. Callers (e.g. resume) may return a
|
|
246
|
+
* Promise; leaving it unobserved turns preparation/switch failures into
|
|
247
|
+
* process-killing unhandled rejections. Inline recovery keeps the picker
|
|
248
|
+
* usable when the host has not already closed it.
|
|
249
|
+
*/
|
|
250
|
+
#dispatchSelect(sessionPath: string): void {
|
|
251
|
+
void Promise.resolve(this.onSelect(sessionPath)).catch((error: unknown) => {
|
|
252
|
+
if (this.#state.kind === "settled") {
|
|
253
|
+
// Host already closed the picker (interactive resume path). Surface the
|
|
254
|
+
// message only if we still have a render surface; controller catch is
|
|
255
|
+
// the primary recovery path after `done()`.
|
|
256
|
+
this.#showError(error instanceof Error ? error.message : String(error));
|
|
257
|
+
this.#requestRender();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.#state = { kind: "browsing" };
|
|
261
|
+
this.#sessionList.setInputFrozen(false);
|
|
262
|
+
this.#showError(error instanceof Error ? error.message : String(error));
|
|
263
|
+
this.#requestRender();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
244
267
|
#settle(selection: SessionSelectionResult): void {
|
|
245
268
|
if (this.#state.kind === "settled") return;
|
|
246
269
|
this.#state = { kind: "settled" };
|
|
@@ -249,7 +272,7 @@ export class SessionSelectorComponent extends Container {
|
|
|
249
272
|
this.#confirmationDialog = null;
|
|
250
273
|
if (dialog) this.removeChild(dialog);
|
|
251
274
|
if (this.onSelection) this.onSelection(selection);
|
|
252
|
-
else if (selection.kind === "selected") this
|
|
275
|
+
else if (selection.kind === "selected") this.#dispatchSelect(selection.path);
|
|
253
276
|
else this.onCancel();
|
|
254
277
|
}
|
|
255
278
|
#cancel(): void {
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { type RunGh, runGhDefault } from "../../../utils/gh";
|
|
2
2
|
|
|
3
3
|
const STATUS_LINE_GH_TIMEOUT_MS = 5_000;
|
|
4
|
+
const C0_C1_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/;
|
|
5
|
+
|
|
6
|
+
function canonicalPrUrl(value: unknown, number: number): string | null {
|
|
7
|
+
if (typeof value !== "string" || C0_C1_CONTROL_CHARACTERS.test(value)) return null;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(value);
|
|
11
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
12
|
+
if (!url.hostname || url.username || url.password || url.search || url.hash) return null;
|
|
13
|
+
|
|
14
|
+
const suffix = `/pull/${number}`;
|
|
15
|
+
if (!url.pathname.endsWith(suffix)) return null;
|
|
16
|
+
const repositoryPath = url.pathname.slice(1, -suffix.length).split("/");
|
|
17
|
+
if (repositoryPath.length !== 2 || repositoryPath.some(component => component === "")) return null;
|
|
18
|
+
|
|
19
|
+
return url.href;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
export async function lookupCurrentPr(runGh: RunGh = runGhDefault): Promise<{ number: number; url: string } | null> {
|
|
6
26
|
try {
|
|
@@ -8,8 +28,9 @@ export async function lookupCurrentPr(runGh: RunGh = runGhDefault): Promise<{ nu
|
|
|
8
28
|
if (result.exitCode !== 0 || result.timedOut) return null;
|
|
9
29
|
|
|
10
30
|
const pr = JSON.parse(result.stdout) as { number?: unknown; url?: unknown };
|
|
11
|
-
if (typeof pr.number !== "number" ||
|
|
12
|
-
|
|
31
|
+
if (typeof pr.number !== "number" || !Number.isSafeInteger(pr.number) || pr.number <= 0) return null;
|
|
32
|
+
const url = canonicalPrUrl(pr.url, pr.number);
|
|
33
|
+
return url ? { number: pr.number, url } : null;
|
|
13
34
|
} catch {
|
|
14
35
|
return null;
|
|
15
36
|
}
|
|
@@ -32,6 +32,7 @@ import { computeIrcSplitWidths, getIrcSidebarSemanticToken } from "../components
|
|
|
32
32
|
import type { IrcObservationRecord } from "../irc-observation-ledger";
|
|
33
33
|
import { interruptHint } from "../shared";
|
|
34
34
|
import { buildAbortDisplayMessage } from "../utils/abort-message";
|
|
35
|
+
import { emitHostStatus } from "../utils/host-status";
|
|
35
36
|
import { consumeInjectedOptimisticSignature } from "../utils/injected-user-submission";
|
|
36
37
|
import { parseIrcMessage } from "../utils/irc-message";
|
|
37
38
|
import { ringTerminalBell } from "../utils/terminal-bell";
|
|
@@ -298,6 +299,7 @@ export class EventController {
|
|
|
298
299
|
}
|
|
299
300
|
|
|
300
301
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
302
|
+
emitHostStatus("working");
|
|
301
303
|
this.ctx.promptSuggestion?.onAgentStart();
|
|
302
304
|
this.#lastIntent = undefined;
|
|
303
305
|
this.#readToolCallArgs.clear();
|
|
@@ -924,6 +926,7 @@ export class EventController {
|
|
|
924
926
|
this.ctx.updateEditorBorderColor();
|
|
925
927
|
this.ctx.ui.requestRender();
|
|
926
928
|
this.#scheduleIdleCompaction();
|
|
929
|
+
emitHostStatus("finished");
|
|
927
930
|
this.sendCompletionNotification();
|
|
928
931
|
this.ctx.promptSuggestion?.onAgentEnd();
|
|
929
932
|
}
|
|
@@ -40,6 +40,7 @@ import { createReadonlySessionManager } from "../../session/session-manager";
|
|
|
40
40
|
import { parseThinkingLevel } from "../../thinking";
|
|
41
41
|
import type { TodoPhase } from "../../tools/todo-write";
|
|
42
42
|
import { setSessionTerminalTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
43
|
+
import { emitHostStatus } from "../utils/host-status";
|
|
43
44
|
import { applyInjectedUserSubmission } from "../utils/injected-user-submission";
|
|
44
45
|
import { classifyHookSelectorBellEvent, ringTerminalBell } from "../utils/terminal-bell";
|
|
45
46
|
import { prepareTranscriptRebuild } from "../utils/ui-helpers";
|
|
@@ -1258,6 +1259,7 @@ export class ExtensionUiController {
|
|
|
1258
1259
|
computeBudget();
|
|
1259
1260
|
|
|
1260
1261
|
ringTerminalBell(classifyHookSelectorBellEvent(title));
|
|
1262
|
+
emitHostStatus("attention");
|
|
1261
1263
|
|
|
1262
1264
|
this.ctx.hookSelector = new HookSelectorComponent(
|
|
1263
1265
|
title,
|
|
@@ -822,6 +822,17 @@ export class InputController {
|
|
|
822
822
|
if (runner?.hasHandlers("input")) {
|
|
823
823
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
824
824
|
if (result?.handled) {
|
|
825
|
+
const handledText = result.text === undefined ? "" : sanitizeText(result.text).trim();
|
|
826
|
+
if (handledText) {
|
|
827
|
+
this.ctx.addMessageToChat({
|
|
828
|
+
role: "custom",
|
|
829
|
+
customType: "extension-input-result",
|
|
830
|
+
content: handledText,
|
|
831
|
+
display: true,
|
|
832
|
+
timestamp: Date.now(),
|
|
833
|
+
});
|
|
834
|
+
this.ctx.ui.requestRender();
|
|
835
|
+
}
|
|
825
836
|
if (this.#canModifyComposer(composer)) {
|
|
826
837
|
this.ctx.editor.setText("");
|
|
827
838
|
}
|
|
@@ -2638,9 +2638,15 @@ export class SelectorController {
|
|
|
2638
2638
|
this.showSelector(done => {
|
|
2639
2639
|
const selector = new SessionSelectorComponent(
|
|
2640
2640
|
sessions,
|
|
2641
|
-
|
|
2641
|
+
sessionPath => {
|
|
2642
|
+
// `onSelect` is a void dispatch boundary: close the picker first, then
|
|
2643
|
+
// observe resume failures so a managed-candidate race (or any other
|
|
2644
|
+
// preparation/switch rejection) surfaces as UI error instead of an
|
|
2645
|
+
// unhandled rejection that can kill the process. Do not auto-retry.
|
|
2642
2646
|
done();
|
|
2643
|
-
|
|
2647
|
+
void this.handleResumeSession(sessionPath).catch(error => {
|
|
2648
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
2649
|
+
});
|
|
2644
2650
|
},
|
|
2645
2651
|
() => {
|
|
2646
2652
|
done();
|
|
@@ -1036,9 +1036,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1036
1036
|
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
1037
1037
|
await this.#goalModeController.beforeGetUserInput();
|
|
1038
1038
|
}
|
|
1039
|
-
const { promise, resolve } = Promise.withResolvers<SubmittedUserInput>();
|
|
1039
|
+
const { promise, resolve, reject } = Promise.withResolvers<SubmittedUserInput>();
|
|
1040
|
+
let unsubscribeStop = () => {};
|
|
1041
|
+
unsubscribeStop = this.onStop(() => {
|
|
1042
|
+
this.onInputCallback = undefined;
|
|
1043
|
+
reject(Object.assign(new Error("Interactive mode stopped"), { code: "cancelled" }));
|
|
1044
|
+
});
|
|
1040
1045
|
this.onInputCallback = input => {
|
|
1041
1046
|
this.onInputCallback = undefined;
|
|
1047
|
+
unsubscribeStop();
|
|
1042
1048
|
resolve(input);
|
|
1043
1049
|
};
|
|
1044
1050
|
this.#goalModeController.scheduleContinuation();
|