Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
66c59b9
feat(runtime): add GitHub Copilot device-flow enrollment
hgaol Aug 21, 2026
38657e4
feat(desktop): sign in to GitHub Copilot with a device grant
hgaol Aug 21, 2026
d13edc7
fix(runtime,desktop): move Copilot enrollment onto the Host OAuth seam
hgaol Aug 21, 2026
16f6520
fix(desktop): restore the GitHub Copilot sign-in in Settings
hgaol Aug 21, 2026
9fff4c6
Merge remote-tracking branch 'upstream/main' into feat/github-copilot…
hgaol Aug 21, 2026
5530fe2
feat(runtime-host): own GitHub Copilot enrollment end to end
hgaol Aug 21, 2026
7d20e18
fix(desktop): adopt the account's own model inventory after sign-in
hgaol Aug 21, 2026
53a72cb
fix(settings): show the device code on the connection re-login notice
hgaol Aug 22, 2026
bc44d6e
Merge branch 'main' of https://github.com/apache/maka into feat/githu…
hgaol Aug 22, 2026
b5da959
fix(desktop): open an account on a model its own live list reported
hgaol Aug 22, 2026
51f8282
Merge branch 'main' of https://github.com/apache/maka into feat/githu…
hgaol Aug 22, 2026
df9b4b4
fix(desktop): let the selected Host decide whether a provider may enroll
hgaol Aug 22, 2026
5619ced
Merge branch 'main' of github.com:apache/maka into feat/github-copilo…
hgaol Aug 22, 2026
49fae7c
fix(runtime): report an unanswered Copilot entitlement check as retry…
hgaol Aug 22, 2026
6c417a5
Merge remote-tracking branch 'upstream/main' into feat/github-copilot…
hgaol Aug 23, 2026
99ac4df
Merge remote-tracking branch 'upstream/main' into feat/github-copilot…
hgaol Aug 23, 2026
0685beb
test(runtime-host): claim the late-request rejection before awaiting …
hgaol Aug 23, 2026
0b9ebaf
Merge remote-tracking branch 'upstream/main' into feat/github-copilot…
hgaol Aug 23, 2026
0cc8898
Merge remote-tracking branch 'upstream/main' into feat/github-copilot…
hgaol Aug 24, 2026
1d460ed
Merge remote-tracking branch 'upstream/main' into feat/github-copilot…
hgaol Aug 24, 2026
85abcaa
Merge branch 'main' of https://github.com/apache/maka into feat/githu…
hgaol Aug 24, 2026
d2f566c
chore: add missing ASF headers on upstream plugin-composition files
hgaol Aug 24, 2026
a4c1fff
chore: retrigger CI (flaky slash-command-menu e2e test)
hgaol Aug 24, 2026
ab38b1f
Merge remote-tracking branch 'upstream/main' into feat/github-copilot…
hgaol Aug 24, 2026
43cc284
Merge branch 'main' of https://github.com/apache/maka into feat/githu…
hgaol Aug 24, 2026
55d1c20
fix(runtime-host): move the compatibility epoch off a value other ope…
hgaol Aug 24, 2026
2ff12f7
test(desktop): retarget the Copilot generation story at the Host sign-in
hgaol Aug 24, 2026
a2e6095
Merge branch 'main' of https://github.com/apache/maka into feat/githu…
hgaol Aug 24, 2026
02ee0bf
docs(astryx): regenerate the surface inventory totals
hgaol Aug 24, 2026
bec8187
Merge branch 'main' of https://github.com/apache/maka into feat/githu…
hgaol Aug 26, 2026
12844b0
fix(desktop): stand the local import down while a device grant is pol…
hgaol Aug 26, 2026
0dbfd9d
Merge branch 'main' of https://github.com/apache/maka into feat/githu…
hgaol Aug 26, 2026
f555b2d
Merge branch 'main' of https://github.com/apache/maka into feat/githu…
hgaol Aug 27, 2026
4096d9d
fix(desktop): ask the Host whether Copilot may enrol before offering …
hgaol Aug 27, 2026
6040b19
Merge origin/main and resolve Copilot entitlement feedback
hgaol Aug 30, 2026
fb4ff2f
chore(desktop): refresh renderer architecture ledger
hgaol Aug 30, 2026
766c8cf
Merge origin/main into feat/github-copilot-device-flow-login
hgaol Aug 31, 2026
0574002
fix(storage): admit github-copilot through the interactive OAuth enro…
hgaol Aug 31, 2026
c8c33b6
Merge origin/main into feat/github-copilot-device-flow-login
hgaol Sep 3, 2026
e5d0aea
fix(oauth): restore selected-Host authority after main merge
hgaol Sep 3, 2026
aa6dd35
fix(oauth): adopt local Copilot credentials through the Host
hgaol Sep 3, 2026
aec5dfb
Merge origin/main into feat/github-copilot-device-flow-login
hgaol Sep 3, 2026
6f27b0c
refactor(desktop): route OAuth settings through the feature services …
hgaol Sep 3, 2026
25d49be
chore(desktop): remove obsolete OAuth bridge surface
hgaol Sep 3, 2026
f859b6d
fix(storybook): bind OAuth stories through connection services
hgaol Sep 3, 2026
3f07d3f
Merge origin/main into feat/github-copilot-device-flow-login
hgaol Sep 3, 2026
24ded23
Merge origin/main into feat/github-copilot-device-flow-login
hgaol Sep 3, 2026
9b91ae6
fix(settings): align Copilot entry copy and OAuth fixture contracts
hgaol Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -156,16 +156,11 @@ test('imports a local GitHub credential through the shared Host account path', a
connectionId: CONNECTION_ID,
modelId: discoveredModelId,
});
assert.deepEqual(await invoke(handlers, 'github-copilot:get-account-state'), {
provider: 'github-copilot',
runtimeState: 'authenticated',
});

assert.deepEqual(await invoke(handlers, 'github-copilot:refresh-tokens'), { ok: true });
assert.deepEqual(await invoke(handlers, 'github-copilot:logout'), { ok: true });
assert.equal(storedSecret, undefined);
assert.equal(catalog.connections[0]?.enabled, false);
assert.equal(changed, 3);
assert.equal(changed, 1);
// Interactive enrollment, account state, refresh, and sign-out belong to the
// Host OAuth coordinator's shared adapter; Desktop registers the local
// credential import and nothing else.
assert.deepEqual([...handlers.keys()], ['github-copilot:connect-existing-login']);
});

async function invoke(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {

assert.deepEqual([...handlers.keys()].sort(), [...RUNTIME_HOST_OAUTH_IPC_CHANNELS].sort());

for (const prefix of ['openai-codex', 'xai-oauth']) {
for (const prefix of ['openai-codex', 'xai-oauth', 'github-copilot']) {
assert.equal(handlers.has(`${prefix}:get-auth-url`), true);
assert.equal(handlers.has(`${prefix}:complete-authorization`), true);
assert.equal(handlers.has(`${prefix}:get-account-state`), true);
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/main/oauth-connection-identities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ import type { OAuthLoginProvider } from '@maka/runtime-host/protocol';
export const INTERACTIVE_OAUTH_CONNECTION_SLUGS = {
'openai-codex': 'codex-subscription',
'xai-oauth': 'xai-oauth',
// Shared with the local `gh` credential import so both routes to a Copilot
// account land on one Connection instead of two.
'github-copilot': 'github-copilot',
} as const satisfies Readonly<Record<OAuthLoginProvider, string>>;
78 changes: 17 additions & 61 deletions apps/desktop/src/main/runtime-host-github-copilot-ipc-main.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import type { ModelInfo } from '@maka/core/llm-connections';
import type { SubscriptionActionResult } from '@maka/core/oauth-subscription';
import {
handleReconnectableRead,
type ReconnectableReadIpcMain,
} from './ipc-reconnect-policy.js';
import { GitHubCopilotSubscriptionService } from './oauth/github-copilot-subscription-service.js';
import type { ReconnectableReadIpcMain } from './ipc-reconnect-policy.js';
import { INTERACTIVE_OAUTH_CONNECTION_SLUGS } from './oauth-connection-identities.js';
import {
disableRuntimeHostAccountConnection,
ensureRuntimeHostAccountConnection,
findRuntimeHostAccountConnection,
runtimeHostAccountCredential,
setRuntimeHostAccountCredential,
synchronizeRuntimeHostAccountConnection,
type RuntimeHostAccountConnectionClient,
} from './runtime-host-account-connection.js';
import type { DesktopRuntimeHostClient } from './runtime-host-client.js';

const PROVIDER = 'github-copilot';
const CONNECTION_SLUG = 'github-copilot';
const CONNECTION_SLUG = INTERACTIVE_OAUTH_CONNECTION_SLUGS[PROVIDER];

type GitHubCopilotClient = RuntimeHostAccountConnectionClient &
Pick<DesktopRuntimeHostClient, 'setCredential'>;
Expand All @@ -36,10 +31,17 @@ export interface RuntimeHostGitHubCopilotIpcDeps {
readonly importExistingLogin?: () => Promise<ImportedGitHubCopilotCredential>;
}

/** Keeps local `gh` discovery in Desktop while committing its credential only to the Host vault. */
export function registerRuntimeHostGitHubCopilotIpc(
deps: RuntimeHostGitHubCopilotIpcDeps,
): void {
/**
* Desktop owns exactly one thing for GitHub Copilot: importing a credential
* that already exists on this machine (`gh` / a compatible PAT). Interactive
* enrollment is not here — the device grant runs through the Host's OAuth
* coordinator like every other account login, so there is one authority that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep local credential import discovery-only

Reachability: ② reasonable concurrent or multi-client path. The local adapter currently performs entitlement discovery in Desktop, then this handler reads the Host's current credential revision and commits the imported secret. If import A is waiting on model discovery while an interactive login B commits through HostOAuthCoordinator, A can resume, adopt B's latest revision as its expected value, and overwrite the newer login. A remote Host also validates the import through the Desktop network environment rather than the Host transport.

The clean final state is for Desktop to return only the locally discovered raw credential and for a Host adoption operation to own entitlement, generation/supersede, and the atomic credential commit alongside interactive login. That closes the race and preserves one network and credential authority without adding another Desktop guard.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reproduced on exact head 02ee0bf874912102984991b31ff64c588649506e: the local import can still replace a newer interactive login.

I paused local import A during its Desktop-side model discovery, committed interactive login B through the Host, and then resumed A. The handler queried B's current credential revision, used that revision as the expected value, and successfully committed A's local secret over B. The observed result was ok: true; the commit expected B's revision 2, so the compare-and-swap guard accepted the overwrite rather than rejecting stale work.

The current test only covers a local import with no overlapping login. Please keep Desktop limited to discovering the local raw credential, and let a Host adoption operation own entitlement, supersession/generation, network transport, and the atomic commit alongside interactive login. That gives both paths one ordering authority.


Posted by an automated review agent operated by @M4n5ter. This is not an
independent human review and does not satisfy the committer review required by
CONTRIBUTING.md. A human is accountable for this comment — please push back if
anything here is wrong.

简体中文

本条评论由 @M4n5ter 运行的自动化审查程序发出。它不构成 CONTRIBUTING.md
所要求的独立人类审查,也不能替代人类审查。有人类对本条评论负责,如有错误请直接指出。

* serializes starts, owns supersede and cancellation, keeps the Host resident
* while polling, uses the configured network transport, and commits the
* credential atomically. Account state, refresh, and sign-out ride the same
* shared `github-copilot:*` channels the coordinator's IPC adapter registers.
*/
export function registerRuntimeHostGitHubCopilotIpc(deps: RuntimeHostGitHubCopilotIpcDeps): void {
const importExistingLogin = deps.importExistingLogin ?? importGitHubCopilotCredential;

deps.ipcMain.handle('github-copilot:connect-existing-login', async () => {
Expand All @@ -53,60 +55,18 @@ export function registerRuntimeHostGitHubCopilotIpc(
imported.result.models.map(({ id }) => id),
);
await setRuntimeHostAccountCredential(deps.client, connection, imported.secret);
await synchronizeRuntimeHostAccountConnection(deps.client, PROVIDER).catch(
() => undefined,
);
await synchronizeRuntimeHostAccountConnection(deps.client, PROVIDER).catch(() => undefined);
deps.emitConnectionListChanged();
return { ok: true as const };
} catch {
return storageFailure('GitHub Copilot login could not be committed to Runtime Host');
}
});

handleReconnectableRead(deps.ipcMain, 'github-copilot:get-account-state', async () => {
const connection = findRuntimeHostAccountConnection(
await deps.client.loadConnectionCatalog(),
PROVIDER,
);
const credential = connection
? await deps.client.queryCredential(runtimeHostAccountCredential(connection))
: null;
return {
provider: PROVIDER,
runtimeState: credential?.configured ? 'authenticated' : 'not_logged_in',
} as const;
});

deps.ipcMain.handle('github-copilot:refresh-tokens', async () => {
const connection = findRuntimeHostAccountConnection(
await deps.client.loadConnectionCatalog(),
PROVIDER,
);
if (!connection) return refreshFailure('GitHub Copilot is not connected');
const credential = await deps.client.queryCredential(
runtimeHostAccountCredential(connection),
);
if (!credential?.configured) return refreshFailure('GitHub Copilot is not connected');
const refreshed = await deps.client.fetchConnectionModels(connection.connectionId);
if (refreshed.kind !== 'committed') {
return refreshFailure(`GitHub Copilot refresh failed: ${refreshed.kind}`);
}
deps.emitConnectionListChanged();
return { ok: true as const };
});

deps.ipcMain.handle('github-copilot:logout', async () => {
try {
await disableRuntimeHostAccountConnection(deps.client, PROVIDER);
} catch {
return storageFailure('GitHub Copilot account could not be removed from Runtime Host');
}
deps.emitConnectionListChanged();
return { ok: true as const };
});
}

async function importGitHubCopilotCredential(): Promise<ImportedGitHubCopilotCredential> {
// The credential never reaches disk in Desktop: the secret is held in memory
// only long enough to be committed to the Host vault.
let secret: string | undefined;
const service = new GitHubCopilotSubscriptionService({
credentialStore: {
Expand All @@ -126,7 +86,3 @@ async function importGitHubCopilotCredential(): Promise<ImportedGitHubCopilotCre
function storageFailure(message: string) {
return { ok: false as const, reason: 'storage_failed' as const, message };
}

function refreshFailure(message: string) {
return { ok: false as const, reason: 'refresh_failed' as const, message };
}
24 changes: 23 additions & 1 deletion apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,9 +961,31 @@ export interface MakaBridge {
};
githubCopilotSubscription: {
connectExistingLogin(host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
isExperimentalEnabled(host?: DesktopRuntimeHostRef): Promise<boolean>;
getAuthUrl(
host?: DesktopRuntimeHostRef,
): Promise<AuthorizationUrlPayload | SubscriptionActionResult>;
openAuthUrl(
authRequestId: string,
host?: DesktopRuntimeHostRef,
): Promise<SubscriptionActionResult>;
completeAuthorization(
authRequestId: string,
host?: DesktopRuntimeHostRef,
): Promise<SubscriptionActionResult>;
cancelAuthorization(
authRequestId?: string,
host?: DesktopRuntimeHostRef,
): Promise<{ ok: true }>;
getAccountState(host?: DesktopRuntimeHostRef): Promise<{
provider: 'github-copilot';
runtimeState: 'not_logged_in' | 'authenticated' | 'refreshing' | 'refresh_failed' | 'storage_failed';
runtimeState:
| 'not_logged_in'
| 'authorizing'
| 'authenticated'
| 'refreshing'
| 'refresh_failed'
| 'storage_failed';
errorMessage?: string;
}>;
refreshTokens(host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult>;
Expand Down
17 changes: 16 additions & 1 deletion apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2250,9 +2250,24 @@ const makaBridge = {
connectExistingLogin(host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult> {
return invokeSelectedRuntimeHost(host, 'github-copilot:connect-existing-login');
},
isExperimentalEnabled(host?: DesktopRuntimeHostRef): Promise<boolean> {
return invokeSelectedRuntimeHost(host, 'github-copilot:is-experimental-enabled');
},
getAuthUrl(host?: DesktopRuntimeHostRef): Promise<AuthorizationUrlPayload | SubscriptionActionResult> {
return invokeSelectedRuntimeHost(host, 'github-copilot:get-auth-url');
},
openAuthUrl(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult> {
return invokeSelectedRuntimeHost(host, 'github-copilot:open-auth-url', authRequestId);
},
completeAuthorization(authRequestId: string, host?: DesktopRuntimeHostRef): Promise<SubscriptionActionResult> {
return invokeSelectedRuntimeHost(host, 'github-copilot:complete-authorization', authRequestId);
},
cancelAuthorization(authRequestId?: string, host?: DesktopRuntimeHostRef): Promise<{ ok: true }> {
return invokeSelectedRuntimeHost(host, 'github-copilot:cancel-authorization', authRequestId);
},
getAccountState(host?: DesktopRuntimeHostRef): Promise<{
provider: 'github-copilot';
runtimeState: 'not_logged_in' | 'authenticated' | 'refreshing' | 'refresh_failed' | 'storage_failed';
runtimeState: 'not_logged_in' | 'authorizing' | 'authenticated' | 'refreshing' | 'refresh_failed' | 'storage_failed';
errorMessage?: string;
}> {
return invokeSelectedRuntimeHost(host, 'github-copilot:get-account-state');
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/locales/settings-provider-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const zhCopy = {
openingBrowser: '打开浏览器…', logout: '退出登录', loggingOut: '退出中…',
copilotSubtitle: '导入兼容的 GitHub 登录;token 不会暴露给渲染进程。', copilotImported: '已导入 GitHub Copilot 订阅账号。',
copilotSetup: '请配置具有 Copilot Requests 权限的 fine-grained PAT;普通 gh auth login 可能不包含该权限。', importing: '导入中…',
copilotSignIn: '使用 GitHub 登录', copilotActionFailed: 'GitHub Copilot 账号操作失败',
reimport: '重新导入', importCredential: '导入兼容凭据', verifying: '验证中…', reverify: '重新验证', removing: '移除中…', removeLocal: '移除本地登录',
loadingAccount: '正在加载账号状态…', authorizing: '请在弹出的浏览器窗口完成登录。', refreshing: '正在刷新访问令牌…', refreshTokenFailed: '令牌刷新失败,请重新登录。',
cardAria: (name: string, status: string | undefined, description: string) => `打开 OAuth 登录:${name}${status ? `,状态:${status}` : ''},${description.replace(/[。.!!??]+$/u, '')}`,
Expand Down Expand Up @@ -308,6 +309,7 @@ const enCopy: ProviderSettingsCopy = {
openingBrowser: 'Opening browser…', logout: 'Sign out', loggingOut: 'Signing out…',
copilotSubtitle: 'Import a compatible GitHub sign-in. The token is never exposed to the renderer.', copilotImported: 'GitHub Copilot subscription account imported.',
copilotSetup: 'Configure a fine-grained PAT with Copilot Requests permission. A normal gh auth login may not include it.', importing: 'Importing…',
copilotSignIn: 'Sign in with GitHub', copilotActionFailed: 'GitHub Copilot account action failed',
reimport: 'Reimport', importCredential: 'Import compatible credentials', verifying: 'Verifying…', reverify: 'Verify again', removing: 'Removing…', removeLocal: 'Remove local sign-in',
loadingAccount: 'Loading account status…', authorizing: 'Complete sign-in in the browser window.', refreshing: 'Refreshing access token…', refreshTokenFailed: 'Token refresh failed. Sign in again.',
cardAria: (name: string, status: string | undefined, description: string) => `Open OAuth sign-in: ${name}${status ? `; status: ${status}` : ''}; ${description.replace(/[。.!!??]+$/u, '')}`,
Expand Down
85 changes: 60 additions & 25 deletions apps/desktop/src/renderer/settings/provider-oauth-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import {
Badge,
Button,
useMountedRef,
useToast,
useUiLocale,
} from '@maka/ui';
import { getProviderSettingsCopy, type ProviderSettingsCopy } from '../locales/settings-provider-copy';
import {
useOAuthLoginFlow,
subscriptionActionErrorMessage,
subscriptionResultMessage,
type OAuthLoginFlowBridge,
type SubscriptionSnapshot,
} from './use-oauth-login-flow';
import { useRuntimeHostSettingsTarget } from './runtime-host-settings-target.js';
Expand Down Expand Up @@ -231,41 +231,76 @@ function SubscriptionLoginPanel(props: {

function GitHubCopilotLoginPanel(props: { onLoginSuccess(): void | Promise<void> }) {
const host = useRuntimeHostSettingsTarget();
const copy = getProviderSettingsCopy(useUiLocale()).oauthSection;
// The shared login-flow controller owns the snapshot refresh, the
// synchronous one-shot pending guard, and the unmount safety; Copilot
// rides it through the direct account flow (one bridge call per action,
// no browser handoff, no logout confirm) instead of owning a separate
// pending-action state machine here (#1042).
const locale = useUiLocale();
const copy = getProviderSettingsCopy(locale).oauthSection;
const toast = useToast();
const mountedRef = useMountedRef();
// The Host owns the device grant, so the panel drives the same browser-
// assisted controller as Codex and xAI: one attempt at a time, superseded
// and cancelled by the Host, with the user code arriving as the state hint.
const flow = useOAuthLoginFlow({
bridge: {
getAccountState: () => window.maka.githubCopilotSubscription.getAccountState(host),
logout: () => window.maka.githubCopilotSubscription.logout(host),
} as OAuthLoginFlowBridge,
bridge: runtimeHostOAuthLoginBridge(window.maka.githubCopilotSubscription, host),
display: { name: 'GitHub Copilot', shortName: 'GitHub Copilot' },
onLoginSuccess: props.onLoginSuccess,
direct: {
login: () => window.maka.githubCopilotSubscription.connectExistingLogin(host),
refreshTokens: () => window.maka.githubCopilotSubscription.refreshTokens(host),
},
});
const refreshTokens = flow.refreshTokens;
const loggedIn = flow.state?.runtimeState === 'authenticated' || flow.state?.runtimeState === 'refreshing';
// Sign-in is always offered, exactly as Codex and xAI are: the Host owns the
// enrollment gate and refuses the start with `experimental_disabled`, so the
// renderer must not carry a second copy of that decision.
const [directAction, setDirectAction] = useState<'import' | 'refresh' | null>(null);
const loggedIn = flow.isLoggedIn;
const actionBusy = flow.actionBusy || directAction !== null;
// Importing an existing `gh` credential and re-verifying it are single main
// process calls with no browser handoff, so they run beside the controller
// rather than through its authRequestId lifecycle.
const runDirectAction = async (
action: 'import' | 'refresh',
call: () => Promise<{ ok: boolean; message?: string }>,
) => {
if (actionBusy) return;
setDirectAction(action);
try {
const result = await call();
if (!mountedRef.current) return;
if (!result.ok) {
toast.error(
copy.copilotActionFailed,
subscriptionResultMessage(result.message, copy.copilotActionFailed, locale),
);
}
await flow.refresh();
if (result.ok && action === 'import' && mountedRef.current) await props.onLoginSuccess();
} catch (error) {
if (mountedRef.current) {
toast.error(copy.copilotActionFailed, subscriptionActionErrorMessage(error, locale));
}
} finally {
if (mountedRef.current) setDirectAction(null);
}
};
return (
<VStack gap={3} data-status={flow.runtimeState}>
<Text type="body">
{loggedIn
? copy.copilotImported
: flow.state?.runtimeState === 'refresh_failed' || flow.state?.runtimeState === 'storage_failed'
? flow.state.errorMessage
: copy.copilotSetup}
{loggedIn ? copy.copilotImported : (flow.errorMessage ?? copy.copilotSetup)}
</Text>
{flow.stateHint && (
<Text type="supporting" color="secondary" data-testid="github-copilot-device-code">
{copy.deviceCode} {flow.stateHint}
</Text>
)}
<HStack gap={2} hAlign="end">
<Button variant="primary" onClick={() => void flow.startLogin()} isDisabled={flow.actionBusy} label={flow.pendingAction === 'login' ? copy.importing : loggedIn ? copy.reimport : copy.importCredential} />
{!loggedIn && (
<Button variant="primary" onClick={() => void flow.startLogin()} isDisabled={actionBusy} label={flow.pendingAction === 'login' ? copy.openingBrowser : copy.copilotSignIn} />
)}
<Button
variant="secondary"
onClick={() => void runDirectAction('import', () => window.maka.githubCopilotSubscription.connectExistingLogin(host))}
isDisabled={actionBusy}
label={directAction === 'import' ? copy.importing : loggedIn ? copy.reimport : copy.importCredential}
/>
{loggedIn && (
<>
<Button variant="secondary" onClick={() => void refreshTokens?.()} isDisabled={flow.actionBusy} label={flow.pendingAction === 'refresh' ? copy.verifying : copy.reverify} />
<Button variant="ghost" onClick={() => void flow.logout()} isDisabled={flow.actionBusy} label={flow.pendingAction === 'logout' ? copy.removing : copy.removeLocal} />
<Button variant="secondary" onClick={() => void runDirectAction('refresh', () => window.maka.githubCopilotSubscription.refreshTokens(host))} isDisabled={actionBusy} label={directAction === 'refresh' ? copy.verifying : copy.reverify} />
<Button variant="ghost" onClick={() => void flow.logout()} isDisabled={actionBusy} label={flow.pendingAction === 'logout' ? copy.removing : copy.removeLocal} />
</>
)}
</HStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export function runtimeHostConnectionsBridge(
}

export function runtimeHostOAuthLoginBridge(
bridge: typeof window.maka.openAiCodex | typeof window.maka.xaiOAuth,
bridge:
| typeof window.maka.openAiCodex
| typeof window.maka.xaiOAuth
| typeof window.maka.githubCopilotSubscription,
host: DesktopRuntimeHostRef,
): OAuthLoginFlowBridge {
return {
Expand Down
Loading