mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
feat(web): redesign OAuth login dialog layout (#867)
* feat(web): redesign OAuth login dialog layout Lead with a single 'Authorize in browser' button that opens the verification link with the device code embedded; demote manual code entry to a secondary fallback; remove the duplicate open-browser and cancel controls and the redundant footer hints so the step order is clear. Props/emits unchanged; no caller changes needed. * fix(web): align login slash copy * fix(web): keep login dialog open on backdrop click
This commit is contained in:
parent
49183d8729
commit
d1dc2a3e77
7 changed files with 138 additions and 121 deletions
5
.changeset/redesign-login-dialog.md
Normal file
5
.changeset/redesign-login-dialog.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Redesign the web OAuth login dialog: lead with a single "Authorize in browser" button that opens the verification link with the device code already embedded, demote manual code entry to a clearly secondary fallback, and drop the duplicate open-browser and cancel controls so the order of steps is unambiguous.
|
||||
5
.changeset/web-login-command-copy.md
Normal file
5
.changeset/web-login-command-copy.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix the web login slash command description to match the browser authorization flow.
|
||||
|
|
@ -158,12 +158,6 @@ async function retryFlow(): Promise<void> {
|
|||
await startFlow();
|
||||
}
|
||||
|
||||
function openBrowser(): void {
|
||||
if (flow.value) {
|
||||
window.open(flow.value.verificationUriComplete, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyCode(): Promise<void> {
|
||||
if (!flow.value) return;
|
||||
try {
|
||||
|
|
@ -224,34 +218,39 @@ function formatSeconds(s: number): string {
|
|||
|
||||
<!-- Device-code step -->
|
||||
<template v-else-if="step === 'device-code' && flow">
|
||||
<div class="dc-body">
|
||||
<div class="dc-instruction">
|
||||
{{ t('login.instruction') }}
|
||||
</div>
|
||||
<div class="nb">
|
||||
<div class="nb-lead">{{ t('login.lead') }}</div>
|
||||
|
||||
<!-- Verification URI -->
|
||||
<div class="dc-uri-row">
|
||||
<a
|
||||
:href="flow.verificationUriComplete"
|
||||
class="dc-uri-btn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:title="flow.verificationUriComplete"
|
||||
>
|
||||
<svg class="dc-link-icon" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M5 2H2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7"/>
|
||||
<path d="M8 1h3v3M11 1 6 6"/>
|
||||
</svg>
|
||||
{{ flow.verificationUri }}
|
||||
</a>
|
||||
</div>
|
||||
<!-- Primary path: open the complete URI (device code already embedded) -->
|
||||
<a
|
||||
class="nb-primary"
|
||||
:href="flow.verificationUriComplete"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ t('login.authorizeInBrowser') }}
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6">
|
||||
<path d="M6 2H2.5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8"/>
|
||||
<path d="M9.5 1.5h3v3M12.5 1.5 7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- User code box -->
|
||||
<div class="dc-code-wrap">
|
||||
<div class="dc-code-label">{{ t('login.deviceCode') }}</div>
|
||||
<div class="dc-code-row">
|
||||
<span class="dc-code-value">{{ flow.userCode }}</span>
|
||||
<button class="dc-copy-btn" :class="{ 'is-copied': copied }" @click="copyCode">
|
||||
<!-- Divider -->
|
||||
<div class="nb-or">{{ t('login.orDivider') }}</div>
|
||||
|
||||
<!-- Fallback path: open the plain URI and type the code manually -->
|
||||
<div class="nb-fallback">
|
||||
<div class="nb-fb-text">
|
||||
{{ t('login.fallbackPrefix') }}<a
|
||||
class="nb-fb-link"
|
||||
:href="flow.verificationUri"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ flow.verificationUri }}</a>{{ t('login.fallbackSuffix') }}
|
||||
</div>
|
||||
<div class="nb-code-row">
|
||||
<span class="nb-code">{{ flow.userCode }}</span>
|
||||
<button class="nb-copy" :class="{ 'is-copied': copied }" @click="copyCode">
|
||||
<template v-if="copied">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="1,6 4,9 11,2"/>
|
||||
|
|
@ -269,25 +268,19 @@ function formatSeconds(s: number): string {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status row -->
|
||||
<div class="dc-status-row">
|
||||
<span class="dc-spinner" :aria-label="t('login.waitingAuth')">
|
||||
<!-- Status -->
|
||||
<div class="nb-status">
|
||||
<span class="nb-spinner" :aria-label="t('login.waitingAuth')">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="var(--blue)" stroke-width="1.5">
|
||||
<circle cx="7" cy="7" r="5" stroke-dasharray="20 12" stroke-linecap="round">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 7 7" to="360 7 7" dur="1s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="dc-status-text">{{ t('login.waitingAuthEllipsis') }}</span>
|
||||
<span class="dc-countdown">{{ formatSeconds(secondsLeft) }}</span>
|
||||
<span class="nb-status-text">{{ t('login.waitingAutoClose') }}</span>
|
||||
<span class="nb-countdown">{{ formatSeconds(secondsLeft) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="act-btn" @click="openBrowser">{{ t('login.openBrowser') }}</button>
|
||||
<button class="act-btn" @click="close">{{ t('login.cancel') }}</button>
|
||||
</div>
|
||||
<div class="footer-hint">{{ t('login.footerHint') }}</div>
|
||||
</template>
|
||||
|
||||
<!-- Success -->
|
||||
|
|
@ -317,7 +310,6 @@ function formatSeconds(s: number): string {
|
|||
<button class="act-btn primary" @click="retryFlow">{{ t('login.retry') }}</button>
|
||||
<button class="act-btn" @click="close">{{ t('login.closeBtn') }}</button>
|
||||
</div>
|
||||
<div class="footer-hint">{{ t('login.escClose') }}</div>
|
||||
</template>
|
||||
|
||||
<!-- Error (endpoint missing or network failure) -->
|
||||
|
|
@ -335,7 +327,6 @@ function formatSeconds(s: number): string {
|
|||
<button class="act-btn primary" @click="retryFlow">{{ t('login.retry') }}</button>
|
||||
<button class="act-btn" @click="close">{{ t('login.closeBtn') }}</button>
|
||||
</div>
|
||||
<div class="footer-hint">{{ t('login.escClose') }}</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
|
@ -420,72 +411,96 @@ function formatSeconds(s: number): string {
|
|||
}
|
||||
|
||||
/* Device-code body */
|
||||
.dc-body {
|
||||
padding: 16px 16px 8px;
|
||||
.nb {
|
||||
padding: 18px 16px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.dc-instruction {
|
||||
.nb-lead {
|
||||
font-size: var(--ui-font-size);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.dc-uri-row { display: flex; }
|
||||
.dc-uri-btn {
|
||||
display: inline-flex;
|
||||
|
||||
/* Primary path: open the complete URI (device code embedded) */
|
||||
.nb-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
background: var(--blue);
|
||||
color: var(--bg);
|
||||
border: 1px solid var(--blue);
|
||||
border-radius: 5px;
|
||||
font-family: var(--mono);
|
||||
font-size: var(--ui-font-size);
|
||||
color: var(--blue);
|
||||
background: var(--soft);
|
||||
border: 1px solid var(--bd);
|
||||
border-radius: 3px;
|
||||
padding: 5px 10px;
|
||||
font-weight: 600;
|
||||
padding: 11px 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
.dc-uri-btn:hover { background: var(--bd); }
|
||||
.dc-link-icon { flex: none; }
|
||||
.nb-primary:hover { background: var(--blue2); border-color: var(--blue2); }
|
||||
|
||||
.dc-code-wrap {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
background: var(--panel);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.dc-code-label {
|
||||
font-size: max(9px, calc(var(--ui-font-size) - 4px));
|
||||
/* "or" divider */
|
||||
.nb-or {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 6px;
|
||||
font-size: max(9px, calc(var(--ui-font-size) - 2.5px));
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.dc-code-row {
|
||||
.nb-or::before,
|
||||
.nb-or::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
/* Fallback path: open plain URI, type the code */
|
||||
.nb-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
}
|
||||
.nb-fb-text {
|
||||
font-size: calc(var(--ui-font-size) - 1.5px);
|
||||
color: var(--dim);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.nb-fb-link {
|
||||
color: var(--blue);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--bd);
|
||||
}
|
||||
.nb-fb-link:hover { border-bottom-color: var(--blue); }
|
||||
.nb-code-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
padding: 9px 11px;
|
||||
}
|
||||
.dc-code-value {
|
||||
font-size: calc(var(--ui-font-size) + 8px);
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
letter-spacing: 0.12em;
|
||||
.nb-code {
|
||||
flex: 1;
|
||||
font-family: var(--mono);
|
||||
font-size: calc(var(--ui-font-size) + 5px);
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
.dc-copy-btn {
|
||||
.nb-copy {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-family: var(--mono);
|
||||
font-size: calc(var(--ui-font-size) - 2.5px);
|
||||
padding: 4px 10px;
|
||||
padding: 5px 11px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 3px;
|
||||
background: none;
|
||||
|
|
@ -494,19 +509,20 @@ function formatSeconds(s: number): string {
|
|||
flex: none;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.dc-copy-btn:hover { background: var(--soft); }
|
||||
.dc-copy-btn.is-copied { color: var(--ok); border-color: var(--ok); }
|
||||
.nb-copy:hover { background: var(--soft); }
|
||||
.nb-copy.is-copied { color: var(--ok); border-color: var(--ok); }
|
||||
|
||||
.dc-status-row {
|
||||
/* Status */
|
||||
.nb-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
padding-top: 11px;
|
||||
border-top: 1px solid var(--line2);
|
||||
}
|
||||
.dc-spinner { display: flex; align-items: center; }
|
||||
.dc-status-text { font-size: var(--ui-font-size); color: var(--dim); flex: 1; }
|
||||
.dc-countdown {
|
||||
.nb-spinner { display: flex; align-items: center; }
|
||||
.nb-status-text { font-size: calc(var(--ui-font-size) - 1.5px); color: var(--dim); flex: 1; }
|
||||
.nb-countdown {
|
||||
font-size: calc(var(--ui-font-size) - 2.5px);
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
|
@ -536,16 +552,6 @@ function formatSeconds(s: number): string {
|
|||
}
|
||||
.act-btn.primary:hover { background: var(--blue2); }
|
||||
|
||||
/* Footer */
|
||||
.footer-hint {
|
||||
padding: 6px 14px;
|
||||
font-size: max(9px, calc(var(--ui-font-size) - 3.5px));
|
||||
color: var(--faint);
|
||||
border-top: 1px solid var(--line2);
|
||||
background: var(--panel);
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.backdrop {
|
||||
align-items: stretch;
|
||||
|
|
@ -563,24 +569,27 @@ function formatSeconds(s: number): string {
|
|||
overflow: hidden;
|
||||
}
|
||||
.center-body,
|
||||
.dc-body {
|
||||
.nb {
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.dc-code-row,
|
||||
.dc-status-row,
|
||||
.nb-code-row,
|
||||
.nb-status,
|
||||
.actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dc-code-value {
|
||||
.nb-code {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.dc-copy-btn {
|
||||
.nb-copy {
|
||||
min-height: 34px;
|
||||
}
|
||||
.dc-status-text {
|
||||
.nb-primary {
|
||||
min-height: 44px;
|
||||
}
|
||||
.nb-status-text {
|
||||
min-width: 0;
|
||||
}
|
||||
.actions {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default {
|
|||
clear: { desc: 'Clear and start a new session' },
|
||||
model: { desc: 'Switch model' },
|
||||
provider: { desc: 'Manage providers (add / remove / refresh)' },
|
||||
login: { desc: 'Sign in to the platform with an API key' },
|
||||
login: { desc: 'Sign in to Kimi in the browser' },
|
||||
permission: { desc: 'Switch approval mode (manual / auto / yolo)' },
|
||||
plan: { desc: 'Toggle plan mode on/off' },
|
||||
swarm: { desc: 'Toggle swarm mode; /swarm <task> runs a task in swarm' },
|
||||
|
|
|
|||
|
|
@ -2,22 +2,21 @@ export default {
|
|||
title: 'Sign in to Kimi Code',
|
||||
close: 'Close (Esc)',
|
||||
starting: 'Starting authorization flow…',
|
||||
instruction: 'Open the link below in your browser and enter the device code to authorize:',
|
||||
deviceCode: 'Device code',
|
||||
lead: 'Click the button below to authorize in a new browser tab.',
|
||||
authorizeInBrowser: 'Authorize in browser',
|
||||
orDivider: 'or',
|
||||
fallbackPrefix: 'On another device? Open ',
|
||||
fallbackSuffix: ' and enter the device code:',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
waitingAuth: 'Waiting for authorization',
|
||||
waitingAuthEllipsis: 'Waiting for authorization…',
|
||||
openBrowser: 'Open browser',
|
||||
cancel: 'Cancel',
|
||||
footerHint: 'This dialog will close automatically once authorization completes · Esc to close',
|
||||
waitingAutoClose: 'Waiting for authorization, signs in automatically…',
|
||||
success: 'Authorized',
|
||||
successHint: 'Loading, will close automatically…',
|
||||
expiredTitle: 'Authorization code expired',
|
||||
expiredHint: 'Please restart the authorization flow',
|
||||
retry: 'Retry',
|
||||
closeBtn: 'Close',
|
||||
escClose: 'Esc to close',
|
||||
errorTitle: 'The current daemon does not support login yet',
|
||||
errorHint: 'Please upgrade kimi-code and try again',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default {
|
|||
clear: { desc: '清空并新建会话' },
|
||||
model: { desc: '切换模型' },
|
||||
provider: { desc: '管理提供商 (添加/删除/刷新)' },
|
||||
login: { desc: '通过 API Key 登录平台' },
|
||||
login: { desc: '在浏览器中登录 Kimi' },
|
||||
permission: { desc: '切换审批模式 (manual/auto/yolo)' },
|
||||
plan: { desc: '切换计划模式 开/关' },
|
||||
swarm: { desc: '切换 swarm 模式;/swarm <任务> 直接在 swarm 下执行' },
|
||||
|
|
|
|||
|
|
@ -2,22 +2,21 @@ export default {
|
|||
title: '登录 Kimi Code',
|
||||
close: '关闭 (Esc)',
|
||||
starting: '正在启动授权流程…',
|
||||
instruction: '在浏览器中打开以下链接,输入设备码完成授权:',
|
||||
deviceCode: '设备码',
|
||||
lead: '点击下方按钮,在新标签页中完成授权。',
|
||||
authorizeInBrowser: '在浏览器中授权',
|
||||
orDivider: '或者',
|
||||
fallbackPrefix: '换个设备?在浏览器打开 ',
|
||||
fallbackSuffix: ' 输入设备码:',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
waitingAuth: '等待授权',
|
||||
waitingAuthEllipsis: '等待授权…',
|
||||
openBrowser: '打开浏览器',
|
||||
cancel: '取消',
|
||||
footerHint: '授权完成后对话框将自动关闭 · Esc 关闭',
|
||||
waitingAutoClose: '等待授权,完成后自动登录…',
|
||||
success: '已授权',
|
||||
successHint: '正在加载,稍后自动关闭…',
|
||||
expiredTitle: '授权码已过期',
|
||||
expiredHint: '请重新开始授权流程',
|
||||
retry: '重试',
|
||||
closeBtn: '关闭',
|
||||
escClose: 'Esc 关闭',
|
||||
errorTitle: '当前 daemon 暂不支持登录',
|
||||
errorHint: '请升级 kimi-code 后重试',
|
||||
} as const;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue