@gajae-code/coding-agent 0.15.2 → 0.15.3
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 +37 -1
- package/dist/types/config/autorouting-tier-map.d.ts +0 -9
- package/dist/types/config/file-lock.d.ts +7 -1
- package/dist/types/config/keybindings.d.ts +13 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +13 -1
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/action-registry.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +16 -1
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/ui-language.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/broker/session-index.d.ts +18 -0
- package/dist/types/sdk/bus/index.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/cli/session-cli.d.ts +1 -1
- package/dist/types/sdk/lifecycle/service.d.ts +32 -5
- package/dist/types/sdk/session.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/fallback-chain-controller.d.ts +8 -0
- package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
- package/dist/types/tools/ask.d.ts +8 -0
- package/dist/types/utils/clipboard.d.ts +9 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/autorouting-tier-map.ts +0 -9
- package/src/config/file-lock.ts +15 -1
- package/src/config/keybindings.ts +13 -3
- package/src/config/model-registry.ts +132 -21
- package/src/config/model-resolver.ts +2 -1
- package/src/config/models-config-schema.ts +2 -2
- package/src/config/settings-schema.ts +13 -1
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +235 -4
- package/src/coordinator-mcp/server.ts +402 -2
- package/src/dap/client.ts +19 -8
- package/src/gjc-runtime/deep-interview-stage.ts +20 -1
- package/src/gjc-runtime/deep-interview-state.ts +3 -1
- package/src/gjc-runtime/session-state-lock.ts +11 -2
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/client.ts +39 -4
- package/src/main.ts +42 -11
- package/src/modes/action-registry.ts +7 -0
- package/src/modes/components/iterm-pet-transport.ts +4 -2
- package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/tool-status-header.ts +149 -41
- package/src/modes/components/welcome.ts +8 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +141 -7
- package/src/modes/controllers/plan-mode-controller.ts +12 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
- package/src/modes/controllers/selector-controller.ts +11 -2
- package/src/modes/interactive-mode.ts +88 -1
- package/src/modes/shared/oauth-url-copy.ts +33 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/ui-language.ts +44 -3
- package/src/runtime-mcp/oauth-flow.ts +19 -8
- package/src/runtime-mcp/transports/stdio.ts +27 -13
- package/src/sdk/acp/adapter.ts +29 -2
- package/src/sdk/broker/broker.ts +26 -6
- package/src/sdk/broker/lifecycle-ledger.ts +28 -1
- package/src/sdk/broker/lifecycle.ts +664 -5
- package/src/sdk/broker/session-index.ts +246 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/index.ts +135 -38
- package/src/sdk/bus/natives-augment.d.ts +20 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +3 -0
- package/src/sdk/cli/session-cli.ts +34 -7
- package/src/sdk/lifecycle/service.ts +166 -11
- package/src/sdk/mcp/server.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/sdk/protocol/operation-registry.ts +2 -1
- package/src/sdk/session.ts +91 -7
- package/src/session/agent-session.ts +403 -56
- package/src/session/fallback-chain-controller.ts +17 -0
- package/src/setup/frictionless-onboarding.ts +4 -4
- package/src/slash-commands/aside-command.test.ts +366 -0
- package/src/slash-commands/builtin-registry.ts +19 -0
- package/src/slash-commands/helpers/aside.ts +299 -0
- package/src/tools/ask.ts +8 -0
- package/src/utils/clipboard.ts +37 -28
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { resolveMCPOAuthResourceOrigin, resolveMCPOAuthTokenEndpoint } from "@gajae-code/ai/core";
|
|
8
|
-
import { Spacer, Text } from "@gajae-code/tui";
|
|
8
|
+
import { matchesKey, type OverlayHandle, Spacer, Text } from "@gajae-code/tui";
|
|
9
9
|
import { getMCPConfigPath, getProjectDir } from "@gajae-code/utils";
|
|
10
10
|
import type { SourceMeta } from "../../capability/types";
|
|
11
11
|
import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../runtime-mcp";
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
setServerDisabled,
|
|
19
19
|
updateMCPServer,
|
|
20
20
|
} from "../../runtime-mcp/config-writer";
|
|
21
|
-
import { canonicalMCPResourceUri, MCPOAuthFlow } from "../../runtime-mcp/oauth-flow";
|
|
21
|
+
import { canonicalMCPResourceUri, type MCPOAuthConfig, MCPOAuthFlow } from "../../runtime-mcp/oauth-flow";
|
|
22
22
|
import {
|
|
23
23
|
clearSmitheryApiKey,
|
|
24
24
|
createSmitheryCliAuthSession,
|
|
@@ -38,10 +38,13 @@ import type { MCPAuthConfig, MCPServerConfig } from "../../runtime-mcp/types";
|
|
|
38
38
|
import type { OAuthCredential } from "../../session/auth-storage";
|
|
39
39
|
import { shortenPath } from "../../tools/render-utils";
|
|
40
40
|
import { openPath } from "../../utils/open";
|
|
41
|
+
import { CommandPalette, type CommandPaletteEntry } from "../components/command-palette";
|
|
41
42
|
import { MCPAddWizard } from "../components/runtime-mcp-add-wizard";
|
|
42
43
|
import { parseCommandArgs } from "../shared";
|
|
44
|
+
import { buildOAuthLoginAnchor, createOAuthUrlCopyLease } from "../shared/oauth-url-copy";
|
|
43
45
|
import { theme } from "../theme/theme";
|
|
44
46
|
import type { InteractiveModeContext } from "../types";
|
|
47
|
+
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
|
45
48
|
import { groupBySource, parseRemoveArgs, readScopeFlag, showCommandMessage } from "./command-controller-shared";
|
|
46
49
|
|
|
47
50
|
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
|
@@ -70,6 +73,18 @@ interface OAuthFlowResult {
|
|
|
70
73
|
type MCPAddScope = "user" | "project";
|
|
71
74
|
type MCPAddTransport = "http" | "sse";
|
|
72
75
|
|
|
76
|
+
type MCPAuthFlow = Pick<MCPOAuthFlow, "resolvedClientId" | "registeredClientSecret"> & {
|
|
77
|
+
login(): Promise<{ access: string; refresh: string; expires: number }>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type MCPAuthCallbacks = {
|
|
81
|
+
onAuth(info: { url: string; instructions?: string }): void;
|
|
82
|
+
onProgress(message: string): void;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type MCPAuthFlowFactory = (config: MCPOAuthConfig, callbacks: MCPAuthCallbacks) => MCPAuthFlow;
|
|
87
|
+
|
|
73
88
|
type MCPAddParsed = {
|
|
74
89
|
initialName?: string;
|
|
75
90
|
scope: MCPAddScope;
|
|
@@ -379,6 +394,46 @@ export class MCPCommandController {
|
|
|
379
394
|
return { keyword, scope, limit, semantic };
|
|
380
395
|
}
|
|
381
396
|
|
|
397
|
+
#openWizardOAuthCommandPalette(wizard: MCPAddWizard, keyData: string): boolean {
|
|
398
|
+
if (this.ctx.hasOAuthUrlForCopy?.() !== true) return false;
|
|
399
|
+
if (!this.ctx.keybindings.getKeys("app.commandPalette.open").some(key => matchesKey(keyData, key))) return false;
|
|
400
|
+
|
|
401
|
+
let overlayHandle: OverlayHandle | undefined;
|
|
402
|
+
const close = (): void => {
|
|
403
|
+
overlayHandle?.hide();
|
|
404
|
+
this.ctx.ui.setFocus(wizard);
|
|
405
|
+
this.ctx.ui.requestRender(true);
|
|
406
|
+
};
|
|
407
|
+
const copyEntry: CommandPaletteEntry = {
|
|
408
|
+
id: "action:app.clipboard.copyOAuthUrl",
|
|
409
|
+
label: "Copy OAuth URL",
|
|
410
|
+
description: "Copy the pending OAuth authorization URL",
|
|
411
|
+
keybinding: this.ctx.keybindings.getDisplayString("app.clipboard.copyOAuthUrl") || undefined,
|
|
412
|
+
handler: async () => {
|
|
413
|
+
await this.ctx.copyOAuthUrl();
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
const palette = new CommandPalette(
|
|
417
|
+
[copyEntry],
|
|
418
|
+
entry => {
|
|
419
|
+
close();
|
|
420
|
+
void Promise.resolve(entry.handler?.()).catch(error => {
|
|
421
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
close,
|
|
425
|
+
);
|
|
426
|
+
overlayHandle = this.ctx.ui.showOverlay(palette, {
|
|
427
|
+
anchor: "bottom-center",
|
|
428
|
+
width: "100%",
|
|
429
|
+
maxHeight: "100%",
|
|
430
|
+
margin: 0,
|
|
431
|
+
});
|
|
432
|
+
this.ctx.ui.setFocus(palette);
|
|
433
|
+
this.ctx.ui.requestRender();
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
|
|
382
437
|
/**
|
|
383
438
|
* Handle /mcp add - Launch interactive wizard or quick-add from args
|
|
384
439
|
*/
|
|
@@ -424,7 +479,7 @@ export class MCPCommandController {
|
|
|
424
479
|
|
|
425
480
|
try {
|
|
426
481
|
const oauthClientSecret = finalConfig.oauth?.clientSecret ?? "";
|
|
427
|
-
const oauthResult = await this
|
|
482
|
+
const oauthResult = await this.handleOAuthFlow(
|
|
428
483
|
finalConfig.url,
|
|
429
484
|
oauth.authorizationUrl,
|
|
430
485
|
oauth.tokenUrl,
|
|
@@ -475,7 +530,8 @@ export class MCPCommandController {
|
|
|
475
530
|
};
|
|
476
531
|
|
|
477
532
|
// Create wizard with OAuth handler and connection test
|
|
478
|
-
|
|
533
|
+
let wizard: MCPAddWizard;
|
|
534
|
+
wizard = new MCPAddWizard(
|
|
479
535
|
async (name: string, config: MCPServerConfig, scope: "user" | "project") => {
|
|
480
536
|
done();
|
|
481
537
|
await this.#handleWizardComplete(name, config, scope);
|
|
@@ -491,8 +547,23 @@ export class MCPCommandController {
|
|
|
491
547
|
clientId: string,
|
|
492
548
|
clientSecret: string,
|
|
493
549
|
scopes: string,
|
|
550
|
+
signal: AbortSignal,
|
|
494
551
|
) => {
|
|
495
|
-
return await this
|
|
552
|
+
return await this.handleOAuthFlow(
|
|
553
|
+
endpointUrl,
|
|
554
|
+
authUrl,
|
|
555
|
+
tokenUrl,
|
|
556
|
+
clientId,
|
|
557
|
+
clientSecret,
|
|
558
|
+
scopes,
|
|
559
|
+
undefined,
|
|
560
|
+
undefined,
|
|
561
|
+
undefined,
|
|
562
|
+
undefined,
|
|
563
|
+
undefined,
|
|
564
|
+
undefined,
|
|
565
|
+
signal,
|
|
566
|
+
);
|
|
496
567
|
},
|
|
497
568
|
async (config: MCPServerConfig) => {
|
|
498
569
|
return await this.#handleTestConnection(config);
|
|
@@ -500,7 +571,15 @@ export class MCPCommandController {
|
|
|
500
571
|
() => {
|
|
501
572
|
this.ctx.ui.requestRender();
|
|
502
573
|
},
|
|
574
|
+
keyData => this.#openWizardOAuthCommandPalette(wizard, keyData),
|
|
503
575
|
parsed.initialName,
|
|
576
|
+
credentialId => this.#removeManagedOAuthCredential(credentialId),
|
|
577
|
+
(credentialId, error) =>
|
|
578
|
+
this.ctx.showError(
|
|
579
|
+
`Cancelled OAuth cleanup failed for ${credentialId}; the credential may remain stored. ${
|
|
580
|
+
error instanceof Error ? error.message : String(error)
|
|
581
|
+
}`,
|
|
582
|
+
),
|
|
504
583
|
);
|
|
505
584
|
|
|
506
585
|
// Replace editor with wizard
|
|
@@ -517,7 +596,8 @@ export class MCPCommandController {
|
|
|
517
596
|
/**
|
|
518
597
|
* Handle OAuth authentication flow for MCP server
|
|
519
598
|
*/
|
|
520
|
-
|
|
599
|
+
/** Internal OAuth entry point; injectable flow construction keeps controller lifecycle coverage deterministic. */
|
|
600
|
+
async handleOAuthFlow(
|
|
521
601
|
endpointUrl: string,
|
|
522
602
|
authUrl: string,
|
|
523
603
|
tokenUrl: string,
|
|
@@ -528,6 +608,9 @@ export class MCPCommandController {
|
|
|
528
608
|
callbackPath?: string,
|
|
529
609
|
redirectUri?: string,
|
|
530
610
|
oauthMeta?: { issuer?: string; issuerResponseIssSupported?: boolean },
|
|
611
|
+
createFlow: MCPAuthFlowFactory = (config, callbacks) => new MCPOAuthFlow(config, callbacks),
|
|
612
|
+
openBrowser: (url: string) => void = openPath,
|
|
613
|
+
signal?: AbortSignal,
|
|
531
614
|
): Promise<OAuthFlowResult> {
|
|
532
615
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
533
616
|
let parsedAuthUrl: URL;
|
|
@@ -546,10 +629,31 @@ export class MCPCommandController {
|
|
|
546
629
|
|
|
547
630
|
const resolvedClientId = clientId.trim() || parsedAuthUrl.searchParams.get("client_id") || undefined;
|
|
548
631
|
const resolvedClientSecret = clientSecret.trim() || undefined;
|
|
632
|
+
const copyOAuthUrlKey = this.ctx.keybindings?.getDisplayString?.("app.clipboard.copyOAuthUrl") ?? "";
|
|
633
|
+
const copyOAuthUrlHint = copyOAuthUrlKey
|
|
634
|
+
? `Use ${copyOAuthUrlKey} or command palette → Copy OAuth URL to copy this exact URL:`
|
|
635
|
+
: "Use command palette → Copy OAuth URL to copy this exact URL:";
|
|
636
|
+
const oauthUrlCopyLease = createOAuthUrlCopyLease(this.ctx);
|
|
637
|
+
const timeoutController = new AbortController();
|
|
638
|
+
const userController = new AbortController();
|
|
639
|
+
const flowSignal = AbortSignal.any([
|
|
640
|
+
userController.signal,
|
|
641
|
+
timeoutController.signal,
|
|
642
|
+
...(signal ? [signal] : []),
|
|
643
|
+
]);
|
|
644
|
+
let flowFinished = false;
|
|
645
|
+
const cancellationUnsubscribe =
|
|
646
|
+
signal === undefined && typeof this.ctx.ui.addInputListener === "function"
|
|
647
|
+
? this.ctx.ui.addInputListener(data => {
|
|
648
|
+
if (data !== "\x03" && !matchesAppInterrupt(data)) return;
|
|
649
|
+
userController.abort(new Error("OAuth flow cancelled"));
|
|
650
|
+
return { consume: true };
|
|
651
|
+
})
|
|
652
|
+
: undefined;
|
|
549
653
|
|
|
550
654
|
try {
|
|
551
655
|
// Create OAuth flow
|
|
552
|
-
const flow =
|
|
656
|
+
const flow = createFlow(
|
|
553
657
|
{
|
|
554
658
|
authorizationUrl: authUrl,
|
|
555
659
|
tokenUrl: tokenEndpoint,
|
|
@@ -569,6 +673,10 @@ export class MCPCommandController {
|
|
|
569
673
|
},
|
|
570
674
|
{
|
|
571
675
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
676
|
+
if (flowFinished || flowSignal.aborted) return;
|
|
677
|
+
queueMicrotask(() => {
|
|
678
|
+
if (!flowFinished && !flowSignal.aborted) oauthUrlCopyLease.replace(info.url);
|
|
679
|
+
});
|
|
572
680
|
// Show auth URL prominently in chat
|
|
573
681
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
574
682
|
this.ctx.chatContainer.addChild(
|
|
@@ -593,7 +701,7 @@ export class MCPCommandController {
|
|
|
593
701
|
this.ctx.ui.requestRender();
|
|
594
702
|
// Try to open browser automatically
|
|
595
703
|
try {
|
|
596
|
-
|
|
704
|
+
openBrowser(info.url);
|
|
597
705
|
|
|
598
706
|
// Show confirmation that browser should open
|
|
599
707
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
@@ -604,10 +712,10 @@ export class MCPCommandController {
|
|
|
604
712
|
this.ctx.chatContainer.addChild(
|
|
605
713
|
new Text(theme.fg("muted", "Alternative if browser did not open:"), 1, 0),
|
|
606
714
|
);
|
|
715
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("success", copyOAuthUrlHint), 1, 0));
|
|
607
716
|
this.ctx.chatContainer.addChild(
|
|
608
|
-
new Text(theme.fg("
|
|
717
|
+
new Text(theme.fg("accent", buildOAuthLoginAnchor(info.url)), 1, 0),
|
|
609
718
|
);
|
|
610
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", info.url), 1, 0));
|
|
611
719
|
this.ctx.ui.requestRender();
|
|
612
720
|
} catch (_error) {
|
|
613
721
|
// Show error if browser doesn't open
|
|
@@ -615,10 +723,10 @@ export class MCPCommandController {
|
|
|
615
723
|
this.ctx.chatContainer.addChild(
|
|
616
724
|
new Text(theme.fg("warning", "→ Could not open browser automatically"), 1, 0),
|
|
617
725
|
);
|
|
726
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("success", copyOAuthUrlHint), 1, 0));
|
|
618
727
|
this.ctx.chatContainer.addChild(
|
|
619
|
-
new Text(theme.fg("
|
|
728
|
+
new Text(theme.fg("accent", buildOAuthLoginAnchor(info.url)), 1, 0),
|
|
620
729
|
);
|
|
621
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", info.url), 1, 0));
|
|
622
730
|
this.ctx.ui.requestRender();
|
|
623
731
|
}
|
|
624
732
|
},
|
|
@@ -627,11 +735,26 @@ export class MCPCommandController {
|
|
|
627
735
|
this.ctx.chatContainer.addChild(new Text(theme.fg("muted", message), 1, 0));
|
|
628
736
|
this.ctx.ui.requestRender();
|
|
629
737
|
},
|
|
738
|
+
signal: flowSignal,
|
|
630
739
|
},
|
|
631
740
|
);
|
|
632
741
|
|
|
633
742
|
// Execute OAuth flow with 5 minute timeout
|
|
634
|
-
const
|
|
743
|
+
const { promise: timeoutPromise, reject: rejectTimeout } = Promise.withResolvers<never>();
|
|
744
|
+
const timeoutId = setTimeout(
|
|
745
|
+
() => {
|
|
746
|
+
timeoutController.abort(new Error("OAuth flow timed out after 5 minutes"));
|
|
747
|
+
rejectTimeout(new Error("OAuth flow timed out after 5 minutes"));
|
|
748
|
+
},
|
|
749
|
+
5 * 60 * 1000,
|
|
750
|
+
);
|
|
751
|
+
let credentials: { access: string; refresh: string; expires: number };
|
|
752
|
+
try {
|
|
753
|
+
credentials = await Promise.race([flow.login(), timeoutPromise]);
|
|
754
|
+
} finally {
|
|
755
|
+
flowFinished = true;
|
|
756
|
+
clearTimeout(timeoutId);
|
|
757
|
+
}
|
|
635
758
|
|
|
636
759
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
637
760
|
this.ctx.chatContainer.addChild(new Text(theme.fg("success", "✓ Authorization completed in browser."), 1, 0));
|
|
@@ -641,6 +764,7 @@ export class MCPCommandController {
|
|
|
641
764
|
const credentialId = `mcp_oauth_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
642
765
|
|
|
643
766
|
// Store credentials in auth storage
|
|
767
|
+
flowSignal.throwIfAborted();
|
|
644
768
|
const oauthCredential: OAuthCredential = {
|
|
645
769
|
type: "oauth",
|
|
646
770
|
...credentials,
|
|
@@ -652,6 +776,10 @@ export class MCPCommandController {
|
|
|
652
776
|
|
|
653
777
|
// Store under a synthetic provider name
|
|
654
778
|
await authStorage.set(credentialId, oauthCredential);
|
|
779
|
+
if (flowSignal.aborted) {
|
|
780
|
+
await authStorage.remove(credentialId);
|
|
781
|
+
flowSignal.throwIfAborted();
|
|
782
|
+
}
|
|
655
783
|
|
|
656
784
|
return {
|
|
657
785
|
credentialId,
|
|
@@ -674,6 +802,9 @@ export class MCPCommandController {
|
|
|
674
802
|
} else {
|
|
675
803
|
throw new Error(`OAuth authentication failed: ${errorMsg}`);
|
|
676
804
|
}
|
|
805
|
+
} finally {
|
|
806
|
+
oauthUrlCopyLease.release();
|
|
807
|
+
cancellationUnsubscribe?.();
|
|
677
808
|
}
|
|
678
809
|
}
|
|
679
810
|
|
|
@@ -1363,7 +1494,7 @@ export class MCPCommandController {
|
|
|
1363
1494
|
|
|
1364
1495
|
this.#showMessage(["", theme.fg("muted", `Reauthorizing "${name}"...`), ""].join("\n"));
|
|
1365
1496
|
|
|
1366
|
-
const oauthResult = await this
|
|
1497
|
+
const oauthResult = await this.handleOAuthFlow(
|
|
1367
1498
|
found.config.url,
|
|
1368
1499
|
oauth.authorizationUrl,
|
|
1369
1500
|
oauth.tokenUrl,
|
|
@@ -1705,6 +1836,14 @@ export class MCPCommandController {
|
|
|
1705
1836
|
|
|
1706
1837
|
async #handleSmitheryBrowserLogin(): Promise<boolean> {
|
|
1707
1838
|
const session = await createSmitheryCliAuthSession();
|
|
1839
|
+
const oauthUrlCopyLease = createOAuthUrlCopyLease(this.ctx);
|
|
1840
|
+
const abortController = new AbortController();
|
|
1841
|
+
const interruptUnsubscribe = this.ctx.ui.addInputListener?.(data => {
|
|
1842
|
+
if (data !== "\x03" && !matchesAppInterrupt(data)) return;
|
|
1843
|
+
abortController.abort(new Error("Smithery authorization cancelled."));
|
|
1844
|
+
return { consume: true };
|
|
1845
|
+
});
|
|
1846
|
+
oauthUrlCopyLease.replace(session.authUrl);
|
|
1708
1847
|
const fallbackLoginUrl = getSmitheryLoginUrl();
|
|
1709
1848
|
this.#showMessage(
|
|
1710
1849
|
[
|
|
@@ -1712,7 +1851,7 @@ export class MCPCommandController {
|
|
|
1712
1851
|
theme.bold("Smithery Login"),
|
|
1713
1852
|
theme.fg("muted", "Browser authorization started. Complete auth in your browser."),
|
|
1714
1853
|
theme.fg("dim", "Authorize URL:"),
|
|
1715
|
-
theme.fg("accent", session.authUrl),
|
|
1854
|
+
theme.fg("accent", buildOAuthLoginAnchor(session.authUrl)),
|
|
1716
1855
|
theme.fg("dim", `Fallback: ${fallbackLoginUrl}`),
|
|
1717
1856
|
"",
|
|
1718
1857
|
].join("\n"),
|
|
@@ -1723,11 +1862,16 @@ export class MCPCommandController {
|
|
|
1723
1862
|
// URL is already shown above.
|
|
1724
1863
|
}
|
|
1725
1864
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1865
|
+
try {
|
|
1866
|
+
const apiKey = await this.#waitForSmitheryCliApiKey(session.sessionId, abortController.signal);
|
|
1867
|
+
await this.#validateSmitheryApiKey(apiKey);
|
|
1868
|
+
await saveSmitheryApiKey(apiKey);
|
|
1869
|
+
this.ctx.showStatus("Smithery API key saved.");
|
|
1870
|
+
return true;
|
|
1871
|
+
} finally {
|
|
1872
|
+
oauthUrlCopyLease.release();
|
|
1873
|
+
interruptUnsubscribe?.();
|
|
1874
|
+
}
|
|
1731
1875
|
}
|
|
1732
1876
|
|
|
1733
1877
|
async #promptSmitheryLogin(reason: string): Promise<boolean> {
|
|
@@ -213,6 +213,7 @@ import { TreeSelectorComponent } from "../components/tree-selector";
|
|
|
213
213
|
import { UserMessageSelectorComponent } from "../components/user-message-selector";
|
|
214
214
|
import type { JobsObserver } from "../jobs-observer";
|
|
215
215
|
import type { SessionObserverRegistry } from "../session-observer-registry";
|
|
216
|
+
import { buildOAuthLoginAnchor, createOAuthUrlCopyLease } from "../shared/oauth-url-copy";
|
|
216
217
|
import type { TasksAggregator } from "../tasks-aggregator";
|
|
217
218
|
import type { TranscriptItemRegistry } from "../transcript-item-registry";
|
|
218
219
|
import { acquireResumeProgressLease, type ResumeProgressLease } from "../utils/ui-helpers";
|
|
@@ -3475,6 +3476,11 @@ export class SelectorController {
|
|
|
3475
3476
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
3476
3477
|
const manualInput = this.ctx.oauthManualInput;
|
|
3477
3478
|
const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
|
|
3479
|
+
const copyOAuthUrlKey = this.ctx.keybindings?.getDisplayString?.("app.clipboard.copyOAuthUrl") ?? "";
|
|
3480
|
+
const copyOAuthUrlHint = copyOAuthUrlKey
|
|
3481
|
+
? `${copyOAuthUrlKey} or command palette → Copy OAuth URL copies the URL exactly.`
|
|
3482
|
+
: "Command palette → Copy OAuth URL copies the URL exactly.";
|
|
3483
|
+
const oauthUrlCopyLease = createOAuthUrlCopyLease(this.ctx);
|
|
3478
3484
|
if (providerId === "opencodex") {
|
|
3479
3485
|
this.ctx.showStatus("Checking the local OpenCodex proxy…");
|
|
3480
3486
|
}
|
|
@@ -3483,10 +3489,12 @@ export class SelectorController {
|
|
|
3483
3489
|
providerId as OAuthProvider,
|
|
3484
3490
|
{
|
|
3485
3491
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
3492
|
+
oauthUrlCopyLease.replace(info.url);
|
|
3486
3493
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
3487
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
3488
|
-
const hyperlink =
|
|
3494
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", buildOAuthLoginAnchor(info.url)), 1, 0));
|
|
3495
|
+
const hyperlink = buildOAuthLoginAnchor(info.url, "Click here to login");
|
|
3489
3496
|
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
3497
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("muted", copyOAuthUrlHint), 1, 0));
|
|
3490
3498
|
if (info.instructions) {
|
|
3491
3499
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
3492
3500
|
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
@@ -3551,6 +3559,7 @@ export class SelectorController {
|
|
|
3551
3559
|
} catch (error: unknown) {
|
|
3552
3560
|
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3553
3561
|
} finally {
|
|
3562
|
+
oauthUrlCopyLease.release();
|
|
3554
3563
|
if (useManualInput) {
|
|
3555
3564
|
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
|
|
3556
3565
|
}
|
|
@@ -431,6 +431,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
431
431
|
hookEditor: HookEditorComponent | undefined = undefined;
|
|
432
432
|
lastStatusSpacer: Spacer | undefined = undefined;
|
|
433
433
|
lastStatusText: Text | undefined = undefined;
|
|
434
|
+
#oauthUrlForCopyLeases: Array<{ token: symbol; url: string }> = [];
|
|
434
435
|
fileSlashCommands: Set<string> = new Set();
|
|
435
436
|
skillCommands: Map<string, Skill> = new Map();
|
|
436
437
|
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
@@ -442,6 +443,37 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
442
443
|
#cleanupUnsubscribe?: () => void;
|
|
443
444
|
#itermPetTransport?: ItermPetTransport;
|
|
444
445
|
|
|
446
|
+
waitForAgentEnd(): { promise: Promise<void>; dispose: () => void } {
|
|
447
|
+
const deferred = Promise.withResolvers<void>();
|
|
448
|
+
let disposed = false;
|
|
449
|
+
let agentStarted = false;
|
|
450
|
+
let unsubscribe: (() => void) | undefined;
|
|
451
|
+
const dispose = () => {
|
|
452
|
+
if (disposed) return;
|
|
453
|
+
disposed = true;
|
|
454
|
+
unsubscribe?.();
|
|
455
|
+
unsubscribe = undefined;
|
|
456
|
+
};
|
|
457
|
+
const listener = (event: AgentSessionEvent) => {
|
|
458
|
+
if (disposed) return;
|
|
459
|
+
if (event.type === "agent_start") {
|
|
460
|
+
agentStarted = true;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (event.type !== "agent_end" || !agentStarted) return;
|
|
464
|
+
dispose();
|
|
465
|
+
deferred.resolve();
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const subscribedUnsubscribe = this.session.subscribe(listener);
|
|
469
|
+
unsubscribe = subscribedUnsubscribe;
|
|
470
|
+
if (disposed) {
|
|
471
|
+
subscribedUnsubscribe();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return { promise: deferred.promise, dispose };
|
|
475
|
+
}
|
|
476
|
+
|
|
445
477
|
/**
|
|
446
478
|
* Pet-unavailable warning text. The iTerm transport reason is parenthesized
|
|
447
479
|
* only when a transport actually reported one; non-iTerm terminals and a
|
|
@@ -808,6 +840,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
808
840
|
rightGutterWidth: COMPOSER_RIGHT_GUTTER_WIDTH,
|
|
809
841
|
collapseChangelog: settings.get("collapseChangelog"),
|
|
810
842
|
keyDisplayContext: this.#keyDisplayContext,
|
|
843
|
+
reducedMotion: settings.get("startup.skipLogoAnimation") === true,
|
|
811
844
|
},
|
|
812
845
|
);
|
|
813
846
|
|
|
@@ -1147,9 +1180,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1147
1180
|
}
|
|
1148
1181
|
|
|
1149
1182
|
async getUserInput(): Promise<SubmittedUserInput> {
|
|
1183
|
+
if (this.#stopped || this.#isShuttingDown) {
|
|
1184
|
+
throw Object.assign(new Error("Interactive mode stopped"), { code: "cancelled" });
|
|
1185
|
+
}
|
|
1150
1186
|
if (this.session.getGoalModeState()?.mode === "exiting") {
|
|
1151
1187
|
await this.#goalModeController.beforeGetUserInput();
|
|
1152
1188
|
}
|
|
1189
|
+
const deferredSubmission = this.#inputController.takeDeferredSubmission();
|
|
1190
|
+
if (deferredSubmission) {
|
|
1191
|
+
return deferredSubmission;
|
|
1192
|
+
}
|
|
1153
1193
|
const { promise, resolve, reject } = Promise.withResolvers<SubmittedUserInput>();
|
|
1154
1194
|
let unsubscribeStop = () => {};
|
|
1155
1195
|
unsubscribeStop = this.onStop(() => {
|
|
@@ -1584,6 +1624,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1584
1624
|
stop(): void {
|
|
1585
1625
|
const wasInitialized = this.isInitialized;
|
|
1586
1626
|
this.#stopped = true;
|
|
1627
|
+
this.#inputController.discardDeferredSubmission();
|
|
1587
1628
|
for (const listener of this.#stopListeners) {
|
|
1588
1629
|
try {
|
|
1589
1630
|
listener();
|
|
@@ -1647,6 +1688,16 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1647
1688
|
// `/btw` owns the shared composer while its panel is open. Never persist a
|
|
1648
1689
|
// side-chat draft or pending side-chat images into the main-session draft.
|
|
1649
1690
|
const hadActiveBtw = this.#btwController.hasOpenPanel();
|
|
1691
|
+
const deferredSubmission = this.#inputController.takeDeferredSubmissionForShutdown();
|
|
1692
|
+
if (
|
|
1693
|
+
deferredSubmission &&
|
|
1694
|
+
!hadActiveBtw &&
|
|
1695
|
+
this.editor.getText().length === 0 &&
|
|
1696
|
+
this.pendingImages.length === 0
|
|
1697
|
+
) {
|
|
1698
|
+
this.editor.setText(deferredSubmission.text);
|
|
1699
|
+
this.pendingImages = deferredSubmission.images ? [...deferredSubmission.images] : [];
|
|
1700
|
+
}
|
|
1650
1701
|
const draftText = selectShutdownDraft(this.editor.getText(), hadActiveBtw);
|
|
1651
1702
|
if (hadActiveBtw) this.pendingImages = [];
|
|
1652
1703
|
this.#btwController.dispose();
|
|
@@ -1788,6 +1839,42 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1788
1839
|
this.#uiHelpers.showWarning(message);
|
|
1789
1840
|
}
|
|
1790
1841
|
|
|
1842
|
+
beginOAuthUrlForCopy(url: string): () => void {
|
|
1843
|
+
const token = Symbol("oauth-url-copy");
|
|
1844
|
+
this.#oauthUrlForCopyLeases.push({ token, url });
|
|
1845
|
+
return () => {
|
|
1846
|
+
const index = this.#oauthUrlForCopyLeases.findIndex(lease => lease.token === token);
|
|
1847
|
+
if (index === -1) return;
|
|
1848
|
+
this.#oauthUrlForCopyLeases.splice(index, 1);
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
hasOAuthUrlForCopy(): boolean {
|
|
1853
|
+
return this.#oauthUrlForCopyLeases.length > 0;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
async copyOAuthUrl(): Promise<void> {
|
|
1857
|
+
const pending = this.#oauthUrlForCopyLeases.at(-1);
|
|
1858
|
+
if (pending === undefined) {
|
|
1859
|
+
this.showStatus("No OAuth URL is available to copy.");
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
try {
|
|
1863
|
+
const outcome = await copyToClipboard(pending.url);
|
|
1864
|
+
if (outcome.status === "verified") {
|
|
1865
|
+
this.showStatus("OAuth URL copied to clipboard.");
|
|
1866
|
+
} else if (outcome.reason.includes("failed") || outcome.reason.includes("rejected")) {
|
|
1867
|
+
this.showWarning(
|
|
1868
|
+
`OAuth URL delivery was not confirmed (${outcome.transport}). Use the visible link or copy it manually.`,
|
|
1869
|
+
);
|
|
1870
|
+
} else {
|
|
1871
|
+
this.showStatus(`OAuth URL delivery attempted via ${outcome.transport}; verify it arrived before pasting.`);
|
|
1872
|
+
}
|
|
1873
|
+
} catch {
|
|
1874
|
+
this.showWarning("Failed to copy OAuth URL to clipboard.");
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1791
1878
|
#handleLspStartupEvent(event: LspStartupEvent): void {
|
|
1792
1879
|
this.#updateWelcomeLspServers();
|
|
1793
1880
|
|
|
@@ -1864,7 +1951,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1864
1951
|
workingMessage => renderWorkingMessage(workingMessage, this.#getWorkingMessageAccent()),
|
|
1865
1952
|
message,
|
|
1866
1953
|
getSymbolTheme().spinnerFrames,
|
|
1867
|
-
{ timeDependentColor: true },
|
|
1954
|
+
{ timeDependentColor: true, renderScope: "layout" },
|
|
1868
1955
|
);
|
|
1869
1956
|
this.statusContainer.addChild(this.loadingAnimation);
|
|
1870
1957
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isHyperlinkEnabled } from "../../tui/hyperlink";
|
|
2
|
+
|
|
3
|
+
export interface OAuthUrlCopyLeaseHost {
|
|
4
|
+
beginOAuthUrlForCopy(url: string): () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface OAuthUrlCopyLease {
|
|
8
|
+
replace(url: string): void;
|
|
9
|
+
release(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildOAuthLoginAnchor(url: string, label: string = url, hyperlinks = isHyperlinkEnabled()): string {
|
|
13
|
+
return hyperlinks ? `\x1b]8;;${url}\x07${label}\x1b]8;;\x07` : label;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createOAuthUrlCopyLease(host: OAuthUrlCopyLeaseHost): OAuthUrlCopyLease {
|
|
17
|
+
let releaseCurrent: (() => void) | undefined;
|
|
18
|
+
let released = false;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
replace(url: string): void {
|
|
22
|
+
if (released) return;
|
|
23
|
+
releaseCurrent?.();
|
|
24
|
+
releaseCurrent = host.beginOAuthUrlForCopy(url);
|
|
25
|
+
},
|
|
26
|
+
release(): void {
|
|
27
|
+
if (released) return;
|
|
28
|
+
released = true;
|
|
29
|
+
releaseCurrent?.();
|
|
30
|
+
releaseCurrent = undefined;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
package/src/modes/types.ts
CHANGED
|
@@ -270,6 +270,9 @@ export interface InteractiveModeContext {
|
|
|
270
270
|
showStatus(message: string, options?: { dim?: boolean }): void;
|
|
271
271
|
showError(message: string): void;
|
|
272
272
|
showWarning(message: string): void;
|
|
273
|
+
beginOAuthUrlForCopy(url: string): () => void;
|
|
274
|
+
hasOAuthUrlForCopy(): boolean;
|
|
275
|
+
copyOAuthUrl(): Promise<void>;
|
|
273
276
|
notifyConfigChanged?: () => Promise<void> | void;
|
|
274
277
|
showNewVersionNotification(newVersion: string): void;
|
|
275
278
|
clearEditor(): void;
|
package/src/modes/ui-language.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const UI_LANGUAGES = ["en", "ko"] as const;
|
|
1
|
+
export const UI_LANGUAGES = ["en", "ko", "ja"] as const;
|
|
2
2
|
|
|
3
3
|
export type UiLanguage = (typeof UI_LANGUAGES)[number];
|
|
4
4
|
|
|
@@ -23,6 +23,7 @@ const ENGLISH_STRINGS = {
|
|
|
23
23
|
"settings.language.description": "Language for human-facing interactive UI text",
|
|
24
24
|
"settings.language.english": "English",
|
|
25
25
|
"settings.language.korean": "Korean (한국어)",
|
|
26
|
+
"settings.language.japanese": "Japanese (日本語)",
|
|
26
27
|
"language.current": "Current UI language:",
|
|
27
28
|
"language.changed": "UI language changed to",
|
|
28
29
|
"language.unknown": "Unknown language",
|
|
@@ -51,24 +52,59 @@ const KOREAN_STRINGS: Record<UiStringKey, string> = {
|
|
|
51
52
|
"settings.language.description": "사람이 읽는 대화형 UI 텍스트의 언어",
|
|
52
53
|
"settings.language.english": "English",
|
|
53
54
|
"settings.language.korean": "한국어",
|
|
55
|
+
"settings.language.japanese": "日本語",
|
|
54
56
|
"language.current": "현재 UI 언어:",
|
|
55
57
|
"language.changed": "UI 언어를 다음으로 변경했습니다:",
|
|
56
58
|
"language.unknown": "알 수 없는 언어",
|
|
57
59
|
};
|
|
58
60
|
|
|
61
|
+
const JAPANESE_STRINGS: Record<UiStringKey, string> = {
|
|
62
|
+
"settings.title": "設定",
|
|
63
|
+
"settings.navigationHint": "(Tab キーで切り替え)",
|
|
64
|
+
"settings.selectHint": " Enter: 選択 · Esc: 戻る",
|
|
65
|
+
"settings.preview": "プレビュー:",
|
|
66
|
+
"settings.tab.appearance": "外観",
|
|
67
|
+
"settings.tab.model": "モデル",
|
|
68
|
+
"settings.tab.interaction": "操作",
|
|
69
|
+
"settings.tab.context": "コンテキスト",
|
|
70
|
+
"settings.tab.memory": "メモリ",
|
|
71
|
+
"settings.tab.editing": "編集",
|
|
72
|
+
"settings.tab.tools": "ツール",
|
|
73
|
+
"settings.tab.tasks": "タスク",
|
|
74
|
+
"settings.tab.providers": "プロバイダー",
|
|
75
|
+
"settings.tab.notifications": "通知",
|
|
76
|
+
"settings.tab.plugins": "プラグイン",
|
|
77
|
+
"settings.tab.gjcBundles": "GJC バンドル",
|
|
78
|
+
"settings.language.label": "言語",
|
|
79
|
+
"settings.language.description": "対話型 UI テキストに使う言語",
|
|
80
|
+
"settings.language.english": "English",
|
|
81
|
+
"settings.language.korean": "한국어",
|
|
82
|
+
"settings.language.japanese": "日本語",
|
|
83
|
+
"language.current": "現在の UI 言語:",
|
|
84
|
+
"language.changed": "UI 言語を次に変更しました:",
|
|
85
|
+
"language.unknown": "不明な言語",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const STRINGS: Record<UiLanguage, Record<UiStringKey, string>> = {
|
|
89
|
+
en: ENGLISH_STRINGS,
|
|
90
|
+
ko: KOREAN_STRINGS,
|
|
91
|
+
ja: JAPANESE_STRINGS,
|
|
92
|
+
};
|
|
93
|
+
|
|
59
94
|
/** User selection is authoritative; invalid or unavailable values deterministically fall back to English. */
|
|
60
95
|
export function resolveUiLanguage(value: unknown): UiLanguage {
|
|
61
|
-
return value === "ko"
|
|
96
|
+
return value === "ko" || value === "ja" ? value : "en";
|
|
62
97
|
}
|
|
63
98
|
|
|
64
99
|
export function uiString(language: unknown, key: UiStringKey): string {
|
|
65
|
-
return resolveUiLanguage(language)
|
|
100
|
+
return STRINGS[resolveUiLanguage(language)][key];
|
|
66
101
|
}
|
|
67
102
|
|
|
68
103
|
/** Endonym shown by `/language` and the language submenu. */
|
|
69
104
|
export const UI_LANGUAGE_LABELS: Record<UiLanguage, string> = {
|
|
70
105
|
en: "English",
|
|
71
106
|
ko: "한국어",
|
|
107
|
+
ja: "日本語",
|
|
72
108
|
};
|
|
73
109
|
|
|
74
110
|
/** Spellings accepted by `/language`; the canonical codes stay authoritative. */
|
|
@@ -81,6 +117,11 @@ const UI_LANGUAGE_ALIASES: Readonly<Record<string, UiLanguage>> = {
|
|
|
81
117
|
kor: "ko",
|
|
82
118
|
korean: "ko",
|
|
83
119
|
한국어: "ko",
|
|
120
|
+
ja: "ja",
|
|
121
|
+
jp: "ja",
|
|
122
|
+
jpn: "ja",
|
|
123
|
+
japanese: "ja",
|
|
124
|
+
日本語: "ja",
|
|
84
125
|
};
|
|
85
126
|
|
|
86
127
|
export function parseUiLanguage(value: string): UiLanguage | undefined {
|