diff --git a/packages/client/ui-conversation/src/client/chat/chat-flow.ts b/packages/client/ui-conversation/src/client/chat/chat-flow.ts
index ad98d3aaad..f925ffbebb 100644
--- a/packages/client/ui-conversation/src/client/chat/chat-flow.ts
+++ b/packages/client/ui-conversation/src/client/chat/chat-flow.ts
@@ -1,7 +1,8 @@
/**
* Chat flow derivation: ConversationSnapshot nodes -> render items. Tool
* results group into consecutive-run tool groups (figma step-summary flow,
- * VERTICAL gap10) alternating with narration; everything else passes through.
+ * VERTICAL gap10) alternating with narration. Consecutive retry notices
+ * reuse the first notice's row while projecting the latest retry turn.
* Item identity keys are stable across snapshots so the list parent can
* subscribe to keys only while rows subscribe to content. IconActions ownership
* (last content assistant per turn) is derived here too so ChatView and the
@@ -49,7 +50,7 @@ export function assistantActionsSeqs(nodes: readonly ConversationNode[]): Readon
/**
* Group finalized nodes into the step-summary flow.
* @param nodes - snapshot nodes (surface order).
- * @returns flow items; consecutive tool-results merged into one group keyed by the first seq.
+ * @returns flow items; consecutive tool results and retry notices reuse their first key.
*/
export function deriveChatFlow(nodes: readonly ConversationNode[]): ChatFlowItem[] {
const items: ChatFlowItem[] = []
@@ -63,6 +64,17 @@ export function deriveChatFlow(nodes: readonly ConversationNode[]): ChatFlowItem
} else {
group.push(node)
}
+ } else if (node.kind === 'model-retry') {
+ group = null
+ const previous = items[items.length - 1]
+ if (
+ previous?.kind === 'node'
+ && previous.node.kind === 'model-retry'
+ ) {
+ items[items.length - 1] = { ...previous, node }
+ } else {
+ items.push({ kind: 'node', key: `n${node.seq}`, node })
+ }
} else {
group = null
items.push({ kind: 'node', key: `n${node.seq}`, node })
diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts
index 1ed0b3aa30..1a9c7a1f34 100644
--- a/packages/client/ui-conversation/src/client/locales.ts
+++ b/packages/client/ui-conversation/src/client/locales.ts
@@ -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': '会话层级',
@@ -48,6 +53,13 @@ export const zh = {
'message.unknownBlock': '未知内容块',
'message.stopped': '已停止',
'message.branch': '在新对话中分支',
+ 'message.retry.active': '正在重试模型请求',
+ 'message.retry.cancelled': '模型请求重试已取消',
+ 'message.retry.started': '已重试模型请求',
+ 'message.retry.scheduled': '等待重试模型请求',
+ 'message.retry.status': '{label}({retry}/{maximum}) · {seconds}s',
+ 'message.retry.delay': '重试延迟:',
+ 'message.retry.failure': '失败原因:',
'command.running': '执行中…',
'command.failed': '命令失败',
'command.done': '已完成',
@@ -105,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',
@@ -130,6 +147,13 @@ export const en = {
'message.unknownBlock': 'Unknown content block',
'message.stopped': 'Stopped',
'message.branch': 'Branch into a new conversation',
+ 'message.retry.active': 'Retrying model request',
+ 'message.retry.cancelled': 'Model request retry cancelled',
+ 'message.retry.started': 'Retried model request',
+ 'message.retry.scheduled': 'Waiting to retry model request',
+ 'message.retry.status': '{label} ({retry}/{maximum}) · {seconds}s',
+ 'message.retry.delay': 'Retry delay: ',
+ 'message.retry.failure': 'Failure reason: ',
'command.running': 'Running…',
'command.failed': 'Command failed',
'command.done': 'Completed',
diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
index 6709c4e434..174a258f60 100644
--- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
+++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
@@ -279,7 +279,7 @@ export function InputBar({
// or while the command face is absent with the session).
const accessSelect: ReactNode = command === undefined
? null
- :
+ :
// Mirror-layer decorations: a visible backdrop with transparent text. The
// claim token highlights through behind the textarea glyphs; each U+FFFC
diff --git a/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx b/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx
index 34a652470e..16974880dd 100644
--- a/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx
+++ b/packages/client/ui-conversation/src/client/skeleton/PermissionSelect.tsx
@@ -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
(null)
const [open, setOpen] = useState(false)
+ const [confirmation, setConfirmation] = useState(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 (
-