Merge branch 'master' into codex/responsive-queue-panel
This commit is contained in:
@@ -23,6 +23,11 @@ export const zh = {
|
||||
'input.stop': '停止生成',
|
||||
'input.send': '发送消息',
|
||||
'input.accessMode': '访问模式,当前:{name}',
|
||||
'access.confirm.title': '确认启用 Full access?',
|
||||
'access.confirm.description': '启用 Full access 后,agent 将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任当前任务时使用。',
|
||||
'access.confirm.acknowledge': '我已了解风险,并愿意继续',
|
||||
'access.confirm.cancel': '取消',
|
||||
'access.confirm.enable': '启用 Full access',
|
||||
'hero.headline': '开始构建吧',
|
||||
'hero.chooseWorkspace': '选择工作区',
|
||||
'session.hierarchy': '会话层级',
|
||||
@@ -112,6 +117,11 @@ export const en = {
|
||||
'input.stop': 'Stop generating',
|
||||
'input.send': 'Send message',
|
||||
'input.accessMode': 'Access mode, current: {name}',
|
||||
'access.confirm.title': 'Enable Full access?',
|
||||
'access.confirm.description': 'Full access reduces confirmation steps and lets the agent perform more actions directly, including sensitive operations, file changes, or external commands. Only use it when you trust the current task.',
|
||||
'access.confirm.acknowledge': 'I understand the risks and want to continue',
|
||||
'access.confirm.cancel': 'Cancel',
|
||||
'access.confirm.enable': 'Enable Full access',
|
||||
'hero.headline': 'Let\'s start building',
|
||||
'hero.chooseWorkspace': 'Choose workspace',
|
||||
'session.hierarchy': 'Session hierarchy',
|
||||
|
||||
@@ -279,7 +279,7 @@ export function InputBar({
|
||||
// or while the command face is absent with the session).
|
||||
const accessSelect: ReactNode = command === undefined
|
||||
? null
|
||||
: <PermissionSelect value={permissions} locked={locked} command={command} t={t} />
|
||||
: <PermissionSelect key={sessionId} value={permissions} locked={locked} command={command} t={t} />
|
||||
|
||||
// Mirror-layer decorations: a visible backdrop with transparent text. The
|
||||
// claim token highlights through behind the textarea glyphs; each U+FFFC
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { PermissionSelect as PermissionSelectValue } from '@deepseek-ai/dsh-permission/client'
|
||||
import { Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { Menu, RiskConfirmation } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ComposerBarProps } from '../contract/slots.ts'
|
||||
import css from './PermissionSelect.module.css'
|
||||
|
||||
const FULL_ACCESS = 'danger-full-access'
|
||||
|
||||
/**
|
||||
* Display transform: kebab-case machine names render as title-case labels
|
||||
* (`workspace-write` → `Workspace Write`); non-kebab host-configured names
|
||||
* pass through. Twin of the /permission popup's (client ui-permission) — the
|
||||
* two permission surfaces must show the same text.
|
||||
* pass through. Full access intentionally overrides the machine-name
|
||||
* transform so both permission surfaces use the product label `Full access`;
|
||||
* the warning body remains locale-aware.
|
||||
*/
|
||||
function displayName(name: string): string {
|
||||
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) return name
|
||||
return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
||||
}
|
||||
|
||||
function optionLabel(option: PermissionSelectValue['options'][number]): string {
|
||||
return option.value === FULL_ACCESS ? 'Full access' : displayName(option.name)
|
||||
}
|
||||
|
||||
export interface PermissionSelectProps {
|
||||
value: PermissionSelectValue | undefined
|
||||
locked: boolean
|
||||
@@ -27,49 +34,94 @@ export interface PermissionSelectProps {
|
||||
export function PermissionSelect({ value, locked, command, t }: PermissionSelectProps) {
|
||||
const [pick, setPick] = useState<string | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [confirmation, setConfirmation] = useState<string | null>(null)
|
||||
const [acknowledged, setAcknowledged] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!locked && value !== undefined) return
|
||||
setOpen(false)
|
||||
setAcknowledged(false)
|
||||
setConfirmation(null)
|
||||
}, [locked, value])
|
||||
|
||||
if (value === undefined) return null
|
||||
|
||||
const currentValue = pick ?? value.currentValue
|
||||
const current = value.options.find(option => option.value === currentValue)
|
||||
const busy = pick !== null
|
||||
const busy = pick !== null || confirmation !== null
|
||||
|
||||
const items: MenuEntry[] = value.options
|
||||
.filter(o => o.value !== 'custom')
|
||||
.map(option => ({ id: option.value, label: displayName(option.name) }))
|
||||
.map(option => ({ id: option.value, label: optionLabel(option) }))
|
||||
|
||||
const choose = (id: string): void => {
|
||||
setOpen(false)
|
||||
if (id === value.currentValue) return
|
||||
const submit = (id: string): void => {
|
||||
setPick(id)
|
||||
void command(`/permission ${id}`)
|
||||
.catch(() => false)
|
||||
.then(() => { setPick(null) })
|
||||
}
|
||||
|
||||
const choose = (id: string): void => {
|
||||
setOpen(false)
|
||||
if (id === value.currentValue) return
|
||||
if (id === FULL_ACCESS) {
|
||||
setAcknowledged(false)
|
||||
setConfirmation(id)
|
||||
return
|
||||
}
|
||||
submit(id)
|
||||
}
|
||||
|
||||
const closeConfirmation = (): void => {
|
||||
setAcknowledged(false)
|
||||
setConfirmation(null)
|
||||
}
|
||||
|
||||
const confirmFullAccess = (): void => {
|
||||
if (locked || !acknowledged || confirmation === null) return
|
||||
const id = confirmation
|
||||
closeConfirmation()
|
||||
submit(id)
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
items={items}
|
||||
selectedId={currentValue}
|
||||
onSelect={choose}
|
||||
onClose={() => { setOpen(false) }}
|
||||
side="top"
|
||||
anchor={
|
||||
<button
|
||||
type="button"
|
||||
className={css.trigger}
|
||||
aria-label={t('input.accessMode', { name: displayName(current?.name ?? currentValue) })}
|
||||
title={current?.description}
|
||||
disabled={locked || busy}
|
||||
onClick={() => { setOpen(!open) }}
|
||||
>
|
||||
<span className={css.triggerLabel}>{displayName(current?.name ?? currentValue)}</span>
|
||||
<svg className={css.chevron} viewBox="0 0 12 12" width="12" height="12" aria-hidden>
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<Menu
|
||||
open={open}
|
||||
items={items}
|
||||
selectedId={currentValue}
|
||||
onSelect={choose}
|
||||
onClose={() => { setOpen(false) }}
|
||||
side="top"
|
||||
anchor={
|
||||
<button
|
||||
type="button"
|
||||
className={css.trigger}
|
||||
aria-label={t('input.accessMode', { name: current === undefined ? displayName(currentValue) : optionLabel(current) })}
|
||||
title={current?.description}
|
||||
disabled={locked || busy}
|
||||
onClick={() => { setOpen(!open) }}
|
||||
>
|
||||
<span className={css.triggerLabel}>{current === undefined ? displayName(currentValue) : optionLabel(current)}</span>
|
||||
<svg className={css.chevron} viewBox="0 0 12 12" width="12" height="12" aria-hidden>
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<RiskConfirmation
|
||||
open={confirmation !== null}
|
||||
title={t('access.confirm.title')}
|
||||
description={t('access.confirm.description')}
|
||||
acknowledgeLabel={t('access.confirm.acknowledge')}
|
||||
cancelLabel={t('access.confirm.cancel')}
|
||||
confirmLabel={t('access.confirm.enable')}
|
||||
acknowledged={acknowledged}
|
||||
disabled={locked}
|
||||
onAcknowledgedChange={setAcknowledged}
|
||||
onCancel={closeConfirmation}
|
||||
onConfirm={confirmFullAccess}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user