mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(client-ui-plugin-config): refresh the key badge when the Host reports the credential changed
The card read the credential only when its settings scope published, and a credential is not part of any settings section: a key written from the Models page — which addresses the same reference — left this badge reporting a state the Host had already replaced. It now re-reads on credentials/changed for the reference it watches, and ignores the event for any other reference.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-plugin-config/README.md
|
||||
README.md: 9297379a940d004acc27e2dcb1bb879fa2142f30
|
||||
README.zh.md: 2b3d73bc6e1ccd72cad9fe86acb357c2ab269a41
|
||||
README.md: cd5ea70e7ca8e126418b73447c7ef2f1aed94b4d
|
||||
README.zh.md: 2859fa9b92d622ff36eb71f099289509fcd84fec
|
||||
|
||||
@@ -20,6 +20,8 @@ A card stages what the user types and writes it only when they save. Each contro
|
||||
|
||||
Saving writes each staged field through the client settings scope, which fences every write with the namespace revision it read, so a form that has drifted from the document is refused rather than overwriting a concurrent change. The Host is the only authority on whether a value was accepted — its validators own the constraints no schema can express — so the card reads the section back afterwards and reports a save that did not land, keeping those drafts for the user to correct.
|
||||
|
||||
A key can also be written from another surface — the Models page addresses the same reference — which changes no settings section, so the card re-reads on the Host's credential-changed signal for the reference it watches.
|
||||
|
||||
A field's presence in the raw user layer — not its value — is what marks it overridden; a reset clears that field so it re-inherits the composition layer. Secret-role fields never ride a response, so a key control starts blank, reports only whether one is configured, and writes through the credentials domain rather than the settings section; a blank draft writes nothing and keeps the stored key.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
保存时,每个暂存字段都通过客户端 settings scope 写入,该 scope 用读取时的命名空间 revision 为每次写入设栅,因此已与文档脱节的表单会被拒绝,而不是覆盖并发变更。某个值是否被接受只有 Host 说了算——schema 表达不了的约束归它的校验器所有——因此卡片在写入后回读分节,报告没有落盘的保存,并保留这些草稿供用户修改。
|
||||
|
||||
密钥也可能从别的表层写入——模型页寻址的是同一个引用——而那不改变任何 settings 分节,因此卡片会在 Host 报告它所关注的引用发生变化时重读。
|
||||
|
||||
字段是否被覆盖,取决于它是否出现在原始用户层中,而非取决于它的值;重置会清除该字段,使其重新继承组装层。secret 角色的字段绝不搭乘响应,因此密钥控件初始为空、只报告是否已配置,并经由 credentials 领域而非 settings 分节写入;空草稿不写入任何东西,保留已存密钥。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -55,6 +55,14 @@ export function apply(ctx: ClientContext): void {
|
||||
const agentLoop = new AgentLoopCardController(bindSettingsScope(ctx, { namespace: AGENT_LOOP_NS }))
|
||||
const webSearch = new WebSearchCardController(bindSettingsScope(ctx, { namespace: WEB_SEARCH_NS }), api)
|
||||
|
||||
// The credential a card reports is not part of any settings section, so its
|
||||
// scope publishes nothing when one is written. This is the only signal that
|
||||
// a key written on another surface reached the Host.
|
||||
ctx.effect(
|
||||
() => ctx.on('credentials/changed', (ref) => { webSearch.refreshCredential(ref) }),
|
||||
'ui-plugin-config: credential invalidations',
|
||||
)
|
||||
|
||||
// The section renders the empty line rather than an empty list when no plugin
|
||||
// contributed a card. The count is read once: the renderer caches a root
|
||||
// entry's inject face per registration, so this reports what was registered
|
||||
|
||||
@@ -143,6 +143,19 @@ export class WebSearchCardController {
|
||||
this.store.set(this.projection())
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read after the Host reports a change to the reference this card watches.
|
||||
*
|
||||
* A key can be written from somewhere else — the Models page addresses the
|
||||
* same reference — and the settings section does not change when it is, so
|
||||
* without this the badge keeps reporting a state the Host already replaced.
|
||||
* @param ref - the reference the Host reports as changed.
|
||||
*/
|
||||
refreshCredential(ref: string): void {
|
||||
if (ref !== this.credential.ref) return
|
||||
void this.readCredential()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the face the card's slot registration injects.
|
||||
* @returns the card's snapshot and its form actions.
|
||||
|
||||
@@ -17,14 +17,15 @@ async function bench() {
|
||||
await ctx.plugin(SlotsService).await()
|
||||
const locale = new LocaleService(ctx)
|
||||
ctx.provide('locale', locale)
|
||||
const describeCredentials = vi.fn(() => Promise.resolve({ rpcId: 'c', result: { ok: false, error: {} } }))
|
||||
ctx.provide('connection', {
|
||||
isLoopback: true,
|
||||
api: {
|
||||
settings: { describe: vi.fn(() => Promise.resolve({ rpcId: 's', result: { ok: false, error: {} } })) },
|
||||
credentials: { describe: vi.fn(() => Promise.resolve({ rpcId: 'c', result: { ok: false, error: {} } })) },
|
||||
credentials: { describe: describeCredentials },
|
||||
},
|
||||
} as never)
|
||||
return { ctx, slots: ctx.get('slots') as SlotsService }
|
||||
return { ctx, slots: ctx.get('slots') as SlotsService, describeCredentials }
|
||||
}
|
||||
|
||||
function declareRoot(slots: SlotsService): () => void {
|
||||
@@ -76,6 +77,33 @@ describe('ui-plugin-config apply', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('re-reads the credential when the Host reports the watched reference changed', async () => {
|
||||
const { ctx, slots, describeCredentials } = await bench()
|
||||
declareRoot(slots)
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
await vi.waitFor(() => { expect(describeCredentials).toHaveBeenCalled() })
|
||||
describeCredentials.mockClear()
|
||||
|
||||
// A key written on another surface changes no settings section, so this
|
||||
// event is the only thing that reaches the card.
|
||||
ctx.emit('credentials/changed', 'DEEPSEEK_API_KEY')
|
||||
|
||||
await vi.waitFor(() => { expect(describeCredentials).toHaveBeenCalledTimes(1) })
|
||||
})
|
||||
|
||||
it('ignores a credential change for a reference no card watches', async () => {
|
||||
const { ctx, slots, describeCredentials } = await bench()
|
||||
declareRoot(slots)
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
await vi.waitFor(() => { expect(describeCredentials).toHaveBeenCalled() })
|
||||
describeCredentials.mockClear()
|
||||
|
||||
ctx.emit('credentials/changed', 'SOME_OTHER_KEY')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(describeCredentials).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('registers into a declaration that arrives after apply', async () => {
|
||||
const { ctx, slots } = await bench()
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
|
||||
@@ -436,6 +436,30 @@ describe('WebSearchCardController', () => {
|
||||
expect(credentials.set).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('re-reads when the Host reports the watched reference changed', async () => {
|
||||
const host = stubSettingsScope<WebSearchSettings>()
|
||||
const credentials = credentialsApi(false)
|
||||
const controller = new WebSearchCardController(host.scope, credentials.api)
|
||||
host.publish({ status: 'ready', writable: true, value: {}, user: {} })
|
||||
await vi.waitFor(() => { expect(credentials.describe).toHaveBeenCalled() })
|
||||
credentials.describe.mockClear()
|
||||
|
||||
// Another reference is not this card's business.
|
||||
controller.refreshCredential('OTHER_KEY')
|
||||
expect(credentials.describe).not.toHaveBeenCalled()
|
||||
|
||||
// A key written on another surface reaches this card only through this signal.
|
||||
credentials.describe.mockImplementation(() => Promise.resolve({
|
||||
rpcId: 'c-1' as never,
|
||||
result: { ok: true as const, value: { credentials: { DEEPSEEK_API_KEY: { configured: true, writable: true } } } },
|
||||
}))
|
||||
controller.refreshCredential('DEEPSEEK_API_KEY')
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(controller.inject().hooks.webSearchCard.getSnapshot().apiKeyConfigured).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('addresses the reference the section declares rather than the default', async () => {
|
||||
const host = stubSettingsScope<WebSearchSettings>()
|
||||
const credentials = credentialsApi(false)
|
||||
|
||||
Reference in New Issue
Block a user