merge: include explicit PTC configuration from the CUA base
This commit is contained in:
commit
751c5cc671
17 files changed
+133
-40
No files matched your search
@@ -74,6 +74,16 @@ Source: [`packages/computer-use/computer-use/src/index.ts`](../../packages/compu
|
||||
Browser controls for provider-owned native permission and application decisions.
|
||||
|
||||
```ts cordis-catalog
|
||||
/** Read PTC execution state separately from native permission checks.
|
||||
* @returns Whether the shared runtime retains memory and has an editable profile entry.
|
||||
*/
|
||||
@Remote ptcConfiguration(): { persistent: boolean; writable: boolean }
|
||||
|
||||
/** Persist in-memory PTC execution through the existing profile editor and Loader lifecycle.
|
||||
* @returns Completion after the shared runtime reload; already-persistent runtimes are left intact.
|
||||
*/
|
||||
@Remote async enablePersistentPtc(): Promise<void>
|
||||
|
||||
/**
|
||||
* Read application decisions without waiting for the native helper.
|
||||
* @returns Saved grants and refused applications with their persistence owner.
|
||||
|
||||
@@ -74,6 +74,16 @@ Source: [`packages/computer-use/computer-use/src/index.ts`](../../packages/compu
|
||||
Browser controls for provider-owned native permission and application decisions.
|
||||
|
||||
```ts cordis-catalog
|
||||
/** Read PTC execution state separately from native permission checks.
|
||||
* @returns Whether the shared runtime retains memory and has an editable profile entry.
|
||||
*/
|
||||
@Remote ptcConfiguration(): { persistent: boolean; writable: boolean }
|
||||
|
||||
/** Persist in-memory PTC execution through the existing profile editor and Loader lifecycle.
|
||||
* @returns Completion after the shared runtime reload; already-persistent runtimes are left intact.
|
||||
*/
|
||||
@Remote async enablePersistentPtc(): Promise<void>
|
||||
|
||||
/**
|
||||
* Read application decisions without waiting for the native helper.
|
||||
* @returns Saved grants and refused applications with their persistence owner.
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 7bf328334f27968f
|
||||
zh: 4d68b54f049497d4
|
||||
/deepseek-ai-dsh-client-ui-cua/understand-the-implementation:
|
||||
en: e7c50be06dfd9dc9
|
||||
zh: 416e869438363eec
|
||||
en: d6cf7f20c568ba15
|
||||
zh: 6cfc962d8f787086
|
||||
/deepseek-ai-dsh-client-ui-cua/further-exploration:
|
||||
en: f0fde9f736ade61a
|
||||
zh: 43a534a21bcc1c13
|
||||
|
||||
@@ -45,9 +45,9 @@ This plugin has no configurable fields.
|
||||
<details>
|
||||
<summary>Implementation internals</summary>
|
||||
|
||||
The optional browser contribution mounts the native provider's permission Remote and reuses the Plugins activation and configuration slots. The four setup rows remain visible while unknown fields use placeholders. The default-mode and execution-mode forms, system checks, application decisions, and application icons fill in as each query completes. Application names and actions do not wait for icons; icon positions retain their space. User actions run in order without waiting for pending observation queries. Disposal waits for both actions and queries to settle before releasing the separately owned Remote mount. Query errors stay with the affected section, and activation guidance depends only on system checks. Plugin Manager’s pending state pauses observation during bundle changes and resumes it afterward; late results from the previous activation are discarded. One controller projects queries into the page and setup dialog; reconnects and updates to the Host-reported settings namespace refresh it. Operation notices live in the shell overlay, so closing the page does not remove them. The provider owns permission checks, persisted decisions, and cancellation; the browser owns no authorization state.
|
||||
The optional browser contribution mounts the native provider's permission Remote and reuses the Plugins activation and configuration slots. The four setup rows remain visible while unknown fields use placeholders. The default-mode form, runtime configuration, system checks, application decisions, and application icons fill in as each query completes. Application names and actions do not wait for icons; icon positions retain their space. User actions run in order without waiting for pending observation queries. Disposal waits for both actions and queries to settle before releasing the separately owned Remote mount. Query errors stay with the affected section, and activation guidance depends only on system checks. Plugin Manager’s pending state pauses observation during bundle changes and resumes it afterward; late results from the previous activation are discarded. One controller projects queries into the page and setup dialog; reconnects and updates to the Host-reported settings namespace refresh it. Operation notices live in the shell overlay, so closing the page does not remove them. The provider owns permission checks, persisted decisions, and cancellation; the browser owns no authorization state.
|
||||
|
||||
The detail page reads the effective default (`selectedDefault ?? default`) from the existing `configForms` service. The default button writes only `selectedDefault`; the execution-mode button writes only `executionMode` on `ptc-runtime`. Both use the form's revision-checked settings path and existing HMR. Loading, read-only and unavailable settings cannot submit a change; a failed write remains beside the setting for retry.
|
||||
The detail page reads the effective default (`selectedDefault ?? default`) from the existing `configForms` service. The default button writes only `selectedDefault` through the form's revision-checked settings path. The execution-mode row uses CUA's `ptcConfiguration()` and `enablePersistentPtc()` controls: the Host edits only `ptc-runtime.executionMode` through the existing configuration editor, preserving its ordinary Loader reload lifecycle. Permission checks and runtime state settle independently. Loading, read-only and unavailable settings cannot submit a change; a failed write remains beside the setting for retry.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ kind: "package-reference"
|
||||
|
||||
可选浏览器插件挂载原生 provider 的权限 Remote,复用插件页的启用引导和配置插槽。四排使用条件始终可见,未加载的字段保留占位。默认模式、执行方式、系统权限、应用列表和应用图标分别查询,结果返回后各自显示。应用名称和操作按钮不等待图标,图标位置保留空位。用户操作按顺序执行,不等待尚未完成的查询;卸载时等待操作和查询全部结束,再释放独立持有的 Remote 挂载。查询错误只显示在对应部分,启用引导只等待系统权限结果。面板复用插件管理器的忙碌状态,在启停期间暂停查询,完成后再刷新;上一次启用期间的迟到结果会被丢弃。一个控制器把查询结果投影到页面和设置弹窗;重新连接或 Host 返回的配置 namespace 更新时刷新状态。操作提示挂载到 shell overlay,关闭页面后仍可显示。provider 负责权限检查、持久授权和取消;浏览器不保存授权决定。
|
||||
|
||||
详情页通过现有 `configForms` 服务读取实际默认值(`selectedDefault ?? default`)。默认模式按钮仅写入 `selectedDefault`;执行方式按钮仅写入 `ptc-runtime` 的 `executionMode`。两者沿用表单带版本检查的设置写入流程和现有 HMR。设置仍在加载、只读或不可用时不能提交;写入失败时在设置旁显示错误,供用户重试。
|
||||
详情页通过现有 `configForms` 服务读取实际默认值(`selectedDefault ?? default`)。默认模式按钮仅写入 `selectedDefault`,沿用表单带版本检查的设置写入流程。执行方式一行调用 CUA 的 `ptcConfiguration()` 和 `enablePersistentPtc()`:Host 通过现有配置编辑器仅修改 `ptc-runtime.executionMode`,保留普通 Loader 重载规则。权限检查和执行方式状态独立返回。设置仍在加载、只读或不可用时不能提交;写入失败时在设置旁显示错误,供用户重试。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react'
|
||||
import { Button, Modal, Toast } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
|
||||
import type { ConfigForm, ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { CuaApplicationGrant, CuaSystemPermissionState } from '@deepseek-ai/dsh-cua/types'
|
||||
import type { PermissionFace } from './controller.ts'
|
||||
import type { NS } from './locales.ts'
|
||||
@@ -21,34 +21,33 @@ type DefaultPresetProps = PropsLocale<typeof NS> & Pick<PropsRuntime<'plugins.bu
|
||||
setPtcDefault: () => Promise<boolean>
|
||||
}>
|
||||
|
||||
type PersistentPtcProps = PropsLocale<typeof NS> & Pick<PropsRuntime<'plugins.bundle.config'>, 'busy'> & InjectFace<{
|
||||
hooks: { cuaPtcRuntime: ConfigForm<{ executionMode?: 'per-call' | 'persistent-in-memory' }> }
|
||||
setPersistentPtc: () => Promise<boolean>
|
||||
}>
|
||||
type PersistentPtcProps = PermissionProps & Pick<PropsRuntime<'plugins.bundle.config'>, 'busy'> & { setPersistentPtc: () => Promise<boolean> }
|
||||
|
||||
/** The detail page owns explicit preset and execution-mode settings. */
|
||||
export type PermissionPanelProps = PermissionProps & DefaultPresetProps & PersistentPtcProps
|
||||
|
||||
function DefaultPresetSetting(props: DefaultPresetProps) {
|
||||
const preset = props.useCuaDefaultPreset(value => value)
|
||||
return <ConfigurationSetting kind="default" setting={preset} t={props.t} busy={props.busy === true} save={props.setPtcDefault}
|
||||
return <ConfigurationSetting kind="default" status={preset.status} loading={preset.value === undefined && (props.busy === true || preset.status === 'loading')} writable={preset.writable} t={props.t} busy={props.busy === true} save={props.setPtcDefault}
|
||||
configured={(preset.value?.selectedDefault ?? preset.value?.default) === 'ptc'} />
|
||||
}
|
||||
|
||||
function PersistentPtcSetting(props: PersistentPtcProps) {
|
||||
const runtime = props.useCuaPtcRuntime(value => value)
|
||||
return <ConfigurationSetting kind="persistent" setting={runtime} t={props.t} busy={props.busy === true} save={props.setPersistentPtc}
|
||||
configured={runtime.value?.executionMode === 'persistent-in-memory'} />
|
||||
const runtime = props.useCuaPermissions(value => value.runtime)
|
||||
return <ConfigurationSetting kind="persistent" loading={runtime.value === null && (runtime.busy || runtime.error === null)}
|
||||
status={runtime.busy ? 'loading' : runtime.error === null && runtime.value !== null ? 'ready' : 'unavailable'} writable={!runtime.busy && runtime.value?.writable === true} t={props.t} busy={props.busy === true} save={props.setPersistentPtc}
|
||||
configured={runtime.value?.persistent === true} />
|
||||
}
|
||||
|
||||
function ConfigurationSetting({ kind, setting, configured, save, t, busy }: PropsLocale<typeof NS> & {
|
||||
function ConfigurationSetting({ kind, status, loading, writable, configured, save, t, busy }: PropsLocale<typeof NS> & {
|
||||
kind: 'default' | 'persistent'
|
||||
busy?: boolean
|
||||
setting: ConfigFormSnapshot<unknown>
|
||||
busy: boolean
|
||||
status: 'loading' | 'ready' | 'unavailable'
|
||||
loading: boolean
|
||||
writable: boolean
|
||||
configured: boolean
|
||||
save: () => Promise<boolean>
|
||||
}) {
|
||||
const loading = setting.value === undefined && (busy === true || setting.status === 'loading')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const submit = async (): Promise<void> => {
|
||||
@@ -64,11 +63,11 @@ function ConfigurationSetting({ kind, setting, configured, save, t, busy }: Prop
|
||||
<div className={css.permission}>
|
||||
<div><strong>{t(`${kind}Title`)}</strong><p>{t(`${kind}Body`)}</p></div>
|
||||
{loading ? <span className={css.buttonPlaceholder} aria-hidden />
|
||||
: <Button size="sm" variant={configured ? 'outline' : 'primary'} disabled={busy === true || saving || configured || !setting.writable || setting.status !== 'ready'}
|
||||
: <Button size="sm" variant={configured ? 'outline' : 'primary'} disabled={busy || saving || configured || !writable || status !== 'ready'}
|
||||
onClick={() => { void submit() }}>{t(configured ? `${kind}Set` : `${kind}Confirm`)}</Button>}
|
||||
</div>
|
||||
{!busy && !configured && (setting.status === 'unavailable' ? <p className={css.error} role="alert">{t(`${kind}Unavailable`)}</p>
|
||||
: setting.status === 'ready' && !setting.writable && <p className={css.error} role="alert">{t(`${kind}ReadOnly`)}</p>)}
|
||||
{!busy && !configured && (status === 'unavailable' ? <p className={css.error} role="alert">{t(`${kind}Unavailable`)}</p>
|
||||
: status === 'ready' && !writable && <p className={css.error} role="alert">{t(`${kind}ReadOnly`)}</p>)}
|
||||
{!busy && error !== null && <p className={css.error} role="alert">{error}</p>}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { CuaApplicationId, CuaApplicationState, CuaSystemPermission, CuaSys
|
||||
|
||||
/** Host operations available to the permission UI. */
|
||||
export interface PermissionActions {
|
||||
readRuntime: () => Promise<{ persistent: boolean; writable: boolean }>
|
||||
readSystem: () => Promise<CuaSystemPermissionState>
|
||||
readApplications: () => Promise<CuaApplicationState>
|
||||
readIcons: () => Promise<Record<string, string>>
|
||||
@@ -13,6 +14,7 @@ export interface PermissionActions {
|
||||
retryApproval: (id: CuaApplicationId) => Promise<void>
|
||||
}
|
||||
interface PermissionData {
|
||||
runtime: { persistent: boolean; writable: boolean }
|
||||
system: CuaSystemPermissionState
|
||||
applications: CuaApplicationState
|
||||
icons: Record<string, string>
|
||||
@@ -45,6 +47,7 @@ export interface PermissionFace {
|
||||
export class PermissionController {
|
||||
/** Last observed permissions and the lifetime of the current refresh/action. */
|
||||
readonly state = createSnapshotStore<PermissionView>({
|
||||
runtime: { value: null, busy: false, error: null },
|
||||
system: { value: null, busy: false, error: null },
|
||||
applications: { value: null, busy: false, error: null },
|
||||
icons: { value: null, busy: false, error: null },
|
||||
@@ -93,7 +96,7 @@ export class PermissionController {
|
||||
const sequence = ++this.sequence
|
||||
this.state.update((state) => {
|
||||
state.busy = true
|
||||
for (const key of ['system', 'applications', 'icons'] as const) state[key].busy = true
|
||||
for (const key of ['runtime', 'system', 'applications', 'icons'] as const) state[key].busy = true
|
||||
})
|
||||
this.tail = this.tail.then(async () => {
|
||||
if (!current()) return
|
||||
@@ -108,6 +111,7 @@ export class PermissionController {
|
||||
}
|
||||
const latest = () => current() && this.sequence === sequence
|
||||
const refresh = Promise.all([
|
||||
this.read('runtime', this.actions.readRuntime, latest),
|
||||
this.read('system', this.actions.readSystem, latest),
|
||||
this.read('applications', this.actions.readApplications, latest),
|
||||
this.read('icons', this.actions.readIcons, latest),
|
||||
|
||||
@@ -35,6 +35,9 @@ export async function mountCuaPermissions(ctx: Context, contribution: TypertRemo
|
||||
const ui = ctx.inject(['remote.cuaAccess', 'slots', 'locale'], (ctx) => {
|
||||
ctx.effect(() => ctx.locale.register(NS, { en, zh }))
|
||||
const controller = new PermissionController({
|
||||
readRuntime: async () => {
|
||||
const result = await ctx.remote.cuaAccess.ptcConfiguration(); if (!result.ok) throw result.error; return result.value
|
||||
},
|
||||
readSystem: async () => {
|
||||
const result = await ctx.remote.cuaAccess.systemStatus(); if (!result.ok) throw result.error; return result.value
|
||||
},
|
||||
@@ -60,12 +63,15 @@ export async function mountCuaPermissions(ctx: Context, contribution: TypertRemo
|
||||
inject: () => ({ resolveText: (text: { en: string; [locale: string]: string }) => ctx.locale.resolveText(text) }),
|
||||
}, ApplicationApproval))
|
||||
const preset = ctx.configForms.get<{ default: string; selectedDefault?: string }>('agent-preset-registry')
|
||||
const runtime = ctx.configForms.get<{ executionMode?: 'per-call' | 'persistent-in-memory' }>('ptc-runtime')
|
||||
const settings = {
|
||||
...face,
|
||||
hooks: { ...face.hooks, cuaDefaultPreset: preset, cuaPtcRuntime: runtime },
|
||||
hooks: { ...face.hooks, cuaDefaultPreset: preset },
|
||||
setPtcDefault: () => preset.set('selectedDefault', 'ptc'),
|
||||
setPersistentPtc: () => runtime.set('executionMode', 'persistent-in-memory'),
|
||||
setPersistentPtc: async () => {
|
||||
const result = await ctx.remote.cuaAccess.enablePersistentPtc(); if (!result.ok) throw result.error
|
||||
face.refresh()
|
||||
return true
|
||||
},
|
||||
}
|
||||
ctx.effect(() => () => controller.dispose())
|
||||
ctx.on('connection/reset', face.refresh)
|
||||
|
||||
@@ -8,7 +8,9 @@ const value: CuaSystemPermissionState = { accessibility: true, screenRecording:
|
||||
const applications: CuaApplicationState = {
|
||||
allowedApps: [], blockedApps: [], settingsNamespace: 'cua', persistentAvailable: true }
|
||||
function fixture() {
|
||||
const actions = { readSystem: vi.fn(async () => value), readApplications: vi.fn(async () => applications),
|
||||
const actions = {
|
||||
readRuntime: vi.fn(async () => ({ persistent: false, writable: true })),
|
||||
readSystem: vi.fn(async () => value), readApplications: vi.fn(async () => applications),
|
||||
readIcons: vi.fn(async (): Promise<Record<string, string>> => ({})), request: vi.fn(async () => {}),
|
||||
openSettings: vi.fn(async () => {}), revoke: vi.fn(async () => {}), retryApproval: vi.fn(async () => {}) }
|
||||
const controller = new PermissionController(actions)
|
||||
|
||||
@@ -43,7 +43,6 @@ async function fixture(fail = false) {
|
||||
const preset = createSnapshotStore({ status: 'ready' as const, value: { default: 'standard' },
|
||||
base: {}, user: {}, revision: 1, writable: true })
|
||||
ctx.provide('configForms', { get: (namespace: string) => {
|
||||
if (namespace === 'ptc-runtime') return { ...preset, set: setPersistent }
|
||||
expect(namespace).toBe('agent-preset-registry')
|
||||
return { ...preset, set: setDefault }
|
||||
} })
|
||||
@@ -55,7 +54,10 @@ async function fixture(fail = false) {
|
||||
const openSettings = vi.fn<() => Promise<RemoteResult<void>>>().mockResolvedValue({ ok: true, value: undefined })
|
||||
const revoke = vi.fn<() => Promise<RemoteResult<void>>>().mockResolvedValue({ ok: true, value: undefined })
|
||||
const retryApproval = vi.fn<() => Promise<RemoteResult<void>>>().mockResolvedValue({ ok: true, value: undefined })
|
||||
ctx.provide('remote.cuaAccess', { applications: status, systemStatus, applicationIcons, requestPermission, openSettings, revoke, retryApproval })
|
||||
ctx.provide('remote.cuaAccess', {
|
||||
ptcConfiguration: vi.fn(async () => ({ ok: true, value: { persistent: false, writable: true } })),
|
||||
enablePersistentPtc: async () => { await setPersistent(); return { ok: true, value: undefined } },
|
||||
applications: status, systemStatus, applicationIcons, requestPermission, openSettings, revoke, retryApproval })
|
||||
ctx.provide('locale', new LocaleRuntime(ctx))
|
||||
await ctx.plugin(SlotRegistry)
|
||||
ctx.slots.register({ name: 'root', children: {
|
||||
@@ -128,7 +130,7 @@ it('withdraws the permission Remote and all controls with the optional plugin',
|
||||
expect(f.ctx.slots.entries('plugins.bundle.activation')[0]!.inject!()).not.toHaveProperty('setPersistentPtc')
|
||||
expect(f.setPersistent).not.toHaveBeenCalled()
|
||||
await (settings.setPersistentPtc as () => Promise<boolean>)()
|
||||
expect(f.setPersistent).toHaveBeenCalledExactlyOnceWith('executionMode', 'persistent-in-memory')
|
||||
expect(f.setPersistent).toHaveBeenCalledExactlyOnceWith()
|
||||
await fiber.dispose()
|
||||
expect(f.ctx.slots.entries('conversation.composer')).toHaveLength(0)
|
||||
expect(f.ctx.slots.entries('plugins.bundle.config')).toHaveLength(0)
|
||||
|
||||
@@ -22,6 +22,7 @@ function observe(value: CuaApplicationState & { system: CuaSystemPermissionState
|
||||
|
||||
function fixture() {
|
||||
const state = createSnapshotStore<PermissionView>({
|
||||
runtime: { value: { persistent: false, writable: true }, busy: false, error: null },
|
||||
system: { value: null, busy: true, error: null }, applications: { value: null, busy: true, error: null },
|
||||
icons: { value: null, busy: true, error: null }, busy: true, toast: null,
|
||||
})
|
||||
@@ -29,17 +30,14 @@ function fixture() {
|
||||
status: 'ready', value: { default: 'standard', selectedDefault: 'ptc' },
|
||||
base: {}, user: {}, revision: 1, writable: true,
|
||||
})
|
||||
const runtime = createSnapshotStore<ConfigFormSnapshot<{ executionMode?: 'per-call' | 'persistent-in-memory' }>>({
|
||||
status: 'ready', value: { executionMode: 'per-call' }, base: {}, user: {}, revision: 1, writable: true,
|
||||
})
|
||||
const refresh = vi.fn()
|
||||
const props = { useCuaPermissions: bindSnapshotSelector(state), t: makeTranslate(en),
|
||||
useCuaPtcRuntime: bindSnapshotSelector(runtime), setPersistentPtc: vi.fn(async () => true),
|
||||
setPersistentPtc: vi.fn(async () => true),
|
||||
useCuaDefaultPreset: bindSnapshotSelector(preset), setPtcDefault: vi.fn(async () => true),
|
||||
refresh, setActive: vi.fn((active: boolean) => { if (active) refresh() }), request: vi.fn(), openSettings: vi.fn(), revoke: vi.fn(),
|
||||
retryApproval: vi.fn(), dismissToast: vi.fn(),
|
||||
} satisfies PermissionPanelProps
|
||||
return { state, preset, runtime, props,
|
||||
return { state, preset, props,
|
||||
publish(patch: Partial<PermissionView>) { act(() => { state.set({ ...state.getSnapshot(), ...patch }) }) },
|
||||
}
|
||||
}
|
||||
@@ -367,21 +365,21 @@ it.each([en, zh])('saves persistent PTC only from its own row and retains failur
|
||||
expect(within(row).getByRole('alert').textContent).toBe(dictionary.persistentNotSaved)
|
||||
fireEvent.click(within(row).getByRole('button'))
|
||||
await waitFor(() => { expect(within(row).queryByRole('alert')).toBeNull() })
|
||||
act(() => { f.runtime.set({ ...f.runtime.getSnapshot(), value: { executionMode: 'persistent-in-memory' } }) })
|
||||
f.publish({ runtime: { value: { persistent: true, writable: true }, busy: false, error: null } })
|
||||
expect(within(row).getByRole('button', { name: dictionary.persistentSet }).hasAttribute('disabled')).toBe(true)
|
||||
expect(f.props.setPtcDefault).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('loads the runtime setting independently and reports unavailable and read-only configurations', () => {
|
||||
const f = fixture()
|
||||
f.runtime.set({ ...f.runtime.getSnapshot(), status: 'loading', value: undefined })
|
||||
f.publish({ runtime: { value: null, busy: true, error: null } })
|
||||
render(<PermissionPanel {...f.props} />)
|
||||
const row = screen.getByRole('group', { name: en.persistentTitle })
|
||||
expect(within(row).queryByRole('button')).toBeNull()
|
||||
expect(screen.getByRole('button', { name: en.defaultSet })).toBeTruthy()
|
||||
act(() => { f.runtime.set({ ...f.runtime.getSnapshot(), status: 'unavailable', writable: false }) })
|
||||
f.publish({ runtime: { value: null, busy: false, error: 'offline' } })
|
||||
expect(within(row).getByRole('alert').textContent).toBe(en.persistentUnavailable)
|
||||
act(() => { f.runtime.set({ ...f.runtime.getSnapshot(), status: 'ready', value: {}, writable: false }) })
|
||||
f.publish({ runtime: { value: { persistent: false, writable: false }, busy: false, error: null } })
|
||||
expect(within(row).getByRole('alert').textContent).toBe(en.persistentReadOnly)
|
||||
expect(within(row).getByRole('button').hasAttribute('disabled')).toBe(true)
|
||||
expect(f.props.setPersistentPtc).not.toHaveBeenCalled()
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 577e1b18dd220c73
|
||||
zh: bde0692c6695d749
|
||||
/deepseek-ai-dsh-cua/understand-the-implementation:
|
||||
en: 5f8f14fda4d69173
|
||||
zh: 5493e3ba74ffb7e0
|
||||
en: 727a4ec9b5cd1789
|
||||
zh: 000a6a34b29e1008
|
||||
/deepseek-ai-dsh-cua/further-exploration:
|
||||
en: b8b0aece886fadaa
|
||||
zh: ecd8c88d601f2179
|
||||
|
||||
@@ -49,6 +49,8 @@ Actions return `dispatched:true, verified:false`; read fresh state to verify the
|
||||
<details>
|
||||
<summary>Shared execution and platform ownership</summary>
|
||||
|
||||
The permission Remote also exposes `ptcConfiguration()` and `enablePersistentPtc()` for the detail-page runtime setting. Reads inspect the existing runtime and editable profile entry. Explicit writes use `configEditor.edit()` to preserve other fields and apply normal Loader reload; an already-persistent runtime is left intact. This setting is separate from system and application authorization.
|
||||
|
||||
The provider calls [installCua](src/index.ts) with its prepared helper, connection factory, and guidance. [NativeClient](src/native-client.ts) owns request correlation, byte limits, cancellation acknowledgement, and failure settlement. The platform connector owns startup and connection cleanup. The shared executor retains one connection per Agent and joins in-flight image writes during disposal.
|
||||
|
||||
`agentPreset` defaults to `ptc`, restricting tools and guidance to Agents bound to that preset. The provider uses the existing `agent/created` and `agent-preset/selected` events and Agent-owned scopes, including Agents already present when it loads. Provider unload and Agent disposal release those registrations. Preset definitions may register after the provider; a preset name filters bound Agents and supplies no tools until a matching Agent exists. Set `agentPreset: false` for global registration in standalone compositions without presets. The Host continues to own permissions, native connections and cancellation; preset definitions remain unchanged.
|
||||
|
||||
@@ -49,6 +49,8 @@ kind: "package-library"
|
||||
<details>
|
||||
<summary>共享执行与平台职责</summary>
|
||||
|
||||
权限 Remote 还通过 `ptcConfiguration()` 和 `enablePersistentPtc()` 支持详情页的执行方式设置。查询读取现有执行器和可编辑的 profile 条目。显式写入使用 `configEditor.edit()` 保留其他字段并按正常 Loader 规则重载;已是持久化模式的执行器保持不变。此设置独立于系统权限和应用授权。
|
||||
|
||||
提供方将准备好的 helper、连接工厂和指引传给 [installCua](src/index.ts)。[NativeClient](src/native-client.ts) 负责请求关联、字节上限、取消确认和失败处理。平台连接器负责启动和连接清理。共享执行器为每个 Agent 保留一个连接,并在销毁时等待进行中的图像写入。
|
||||
|
||||
`agentPreset` 默认为 `ptc`,将工具和指引限制到绑定该 preset 的 Agent。提供方使用现有 `agent/created`、`agent-preset/selected` 事件及 Agent 拥有的作用域,也处理启用前已存在的 Agent。提供方卸载和 Agent 销毁都会释放这些注册。preset 定义可以晚于提供方注册;名称用于筛选已绑定的 Agent,在匹配的 Agent 出现前不提供工具。不使用 preset 的独立组合可设置 `agentPreset: false` 来全局注册。Host 仍拥有权限、本机连接和取消操作,preset 定义保持不变。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Browser controls for the provider's application decisions and native permission work. */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-config-editor'
|
||||
import type {} from '@deepseek-ai/dsh-ptc-runtime'
|
||||
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { ApplicationAccess, type AccessOptions } from './application-access.ts'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
|
||||
@@ -40,6 +42,29 @@ export class CuaAccess extends TypertRemoteService {
|
||||
constructor(ctx: Context, private readonly policy: ApplicationAccess,
|
||||
private readonly settingsNamespace: string | null, private readonly control: Control) { super(ctx, 'cuaAccess') }
|
||||
|
||||
/** Read PTC execution state separately from native permission checks.
|
||||
* @returns Whether the shared runtime retains memory and has an editable profile entry.
|
||||
*/
|
||||
@Remote
|
||||
ptcConfiguration(): { persistent: boolean; writable: boolean } {
|
||||
return {
|
||||
persistent: this.ctx.get('ptcRuntime')?.programFormat === 'repl',
|
||||
writable: this.ctx.get('configEditor')?.entries().some(entry => entry.options.id === 'ptc-runtime') ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist in-memory PTC execution through the existing profile editor and Loader lifecycle.
|
||||
* @returns Completion after the shared runtime reload; already-persistent runtimes are left intact.
|
||||
*/
|
||||
@Remote
|
||||
async enablePersistentPtc(): Promise<void> {
|
||||
if (this.ptcConfiguration().persistent) return
|
||||
const editor = this.ctx.get('configEditor')
|
||||
const entry = editor?.entries().find(entry => entry.options.id === 'ptc-runtime')
|
||||
if (editor === undefined || entry === undefined) throw new Error('PTC runtime has no editable profile entry')
|
||||
await editor.edit(entry, current => ({ ...current, executionMode: 'persistent-in-memory' }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Read application decisions without waiting for the native helper.
|
||||
* @returns Saved grants and refused applications with their persistence owner.
|
||||
|
||||
@@ -408,3 +408,24 @@ it('omits session-only grants from the permission page and retains their access
|
||||
expect(state.blockedApps).toEqual([])
|
||||
await expect(f.access.run(f.client, handle, signal, async () => 'still allowed')).resolves.toBe('still allowed')
|
||||
})
|
||||
|
||||
it('configures persistent PTC through the existing profile editor only on explicit request', async () => {
|
||||
const f = await setup()
|
||||
expect(f.ctx.cuaAccess.ptcConfiguration()).toEqual({ persistent: false, writable: false })
|
||||
await expect(f.ctx.cuaAccess.enablePersistentPtc()).rejects.toThrow('no editable profile entry')
|
||||
const entry = { options: { id: 'ptc-runtime' } }
|
||||
const runtime = { programFormat: 'function-body' }
|
||||
const edit = vi.fn(async (received: object, change: (current: Record<string, unknown>) => Record<string, unknown>) => {
|
||||
expect(received).toBe(entry)
|
||||
expect(change({ timeoutMs: 4200 })).toEqual({ timeoutMs: 4200, executionMode: 'persistent-in-memory' })
|
||||
runtime.programFormat = 'repl'
|
||||
})
|
||||
f.ctx.provide('ptcRuntime', runtime)
|
||||
f.ctx.provide('configEditor', { entries: () => [entry], edit })
|
||||
expect(f.ctx.cuaAccess.ptcConfiguration()).toEqual({ persistent: false, writable: true })
|
||||
expect(edit).not.toHaveBeenCalled()
|
||||
await f.ctx.cuaAccess.enablePersistentPtc()
|
||||
expect(f.ctx.cuaAccess.ptcConfiguration()).toEqual({ persistent: true, writable: true })
|
||||
await f.ctx.cuaAccess.enablePersistentPtc()
|
||||
expect(edit).toHaveBeenCalledOnce()
|
||||
})
|
||||
@@ -915,6 +915,18 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
summary: 'Browser controls for provider-owned native permission and application decisions.',
|
||||
description: 'Browser controls for provider-owned native permission and application decisions.',
|
||||
methods: [
|
||||
{
|
||||
signature: '@Remote ptcConfiguration(): { persistent: boolean; writable: boolean }',
|
||||
description: 'Read PTC execution state separately from native permission checks.',
|
||||
parameters: [],
|
||||
returns: 'Whether the shared runtime retains memory and has an editable profile entry.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote async enablePersistentPtc(): Promise<void>',
|
||||
description: 'Persist in-memory PTC execution through the existing profile editor and Loader lifecycle.',
|
||||
parameters: [],
|
||||
returns: 'Completion after the shared runtime reload; already-persistent runtimes are left intact.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote applications(): CuaApplicationState',
|
||||
description: 'Read application decisions without waiting for the native helper.',
|
||||
|
||||
Reference in new issue
Block a user