feat: optimze skill and tool

This commit is contained in:
imccyu
2026-08-13 03:46:20 +08:00
parent 35b51dd15c
commit 499d397631
13 changed files with 132 additions and 30 deletions
@@ -304,6 +304,7 @@ To customize the call card for an ordinary model Tool, query `tool.call.toolview
### Overlays and local entry points ### Overlays and local entry points
- For toasts, status notices, and frame-wide overlays, query `shell.overlay` first; observe its pointer-events and ordering rules. - For toasts, status notices, and frame-wide overlays, query `shell.overlay` first; observe its pointer-events and ordering rules.
- When the selected target is a global overlay Slot, decide whether the UI should be draggable, how the user shows and hides it, and which existing layers it must cover or remain below.
- For small sidebar actions, prefer additive inner Slots such as `sidebar.footer.action`; do not replace the entire sidebar. - For small sidebar actions, prefer additive inner Slots such as `sidebar.footer.action`; do not replace the entire sidebar.
- For supplementary content after a conversation turn, query `conversation.chat.turnTail` and register according to its returned chain selector and fallback rules. - For supplementary content after a conversation turn, query `conversation.chat.turnTail` and register according to its returned chain selector and fallback rules.
+4 -4
View File
@@ -1,6 +1,6 @@
// Web e2e scenario for the opt-in Cordis tools. Record mode drives a real // Web e2e scenario for the opt-in Cordis tools. Record mode drives a real
// model through inspect, define, run, and stop; replay pins the same shipped Web // model through inspect, define, run, and stop; replay pins the same shipped Web
// composition, durable calls, generic rows, the define card's own source view, // composition, durable calls, Cordis-owned rows, the define card's own source view,
// and conversation accessibility tree. // and conversation accessibility tree.
// //
// The approval is never in the fixture. The fixture pins what the MODEL said; // The approval is never in the fixture. The fixture pins what the MODEL said;
@@ -64,7 +64,7 @@ function assertCompleteCordisLifecycle(events: readonly SessionEvent[]): void {
expect(results.every(event => !event.data.message.content[0].isError)).toBe(true) expect(results.every(event => !event.data.message.content[0].isError)).toBe(true)
} }
describe('web e2e: Cordis tools use the generic row variants', () => { describe('web e2e: Cordis tools use their owned cards', () => {
let scaffold: WebScaffold let scaffold: WebScaffold
let browser: Browser let browser: Browser
let page: Page let page: Page
@@ -129,7 +129,7 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
assertCompleteCordisLifecycle(sessionEvents) assertCompleteCordisLifecycle(sessionEvents)
}) })
it.skipIf(MODE === 'record')('renders Cordis lifecycle titles over the generic row mechanics', async () => { it.skipIf(MODE === 'record')('renders localized Cordis lifecycle cards', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-cordis-rows')) onTestFailed(() => saveFailureShot(page, 'web-e2e-cordis-rows'))
await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 }) await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 })
.toBeGreaterThanOrEqual(1) .toBeGreaterThanOrEqual(1)
@@ -153,7 +153,7 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
await runRow.waitFor({ timeout: 10_000 }) await runRow.waitFor({ timeout: 10_000 })
await expect.poll(() => runRow.textContent()).toContain('snap-') await expect.poll(() => runRow.textContent()).toContain('snap-')
const stopRow = page.locator('[data-tool="cordis_stop"]').filter({ hasText: 'Stop dynamic package' }).first() const stopRow = page.locator('[data-tool="cordis_stop"]').filter({ hasText: 'Stop Cordis Plugin' }).first()
await stopRow.waitFor({ timeout: 10_000 }) await stopRow.waitFor({ timeout: 10_000 })
await expect.poll(() => stopRow.textContent()).toContain('snap-') await expect.poll(() => stopRow.textContent()).toContain('snap-')
await expect(stopRow.getAttribute('data-state')).resolves.toBe('ok') await expect(stopRow.getAttribute('data-state')).resolves.toBe('ok')
@@ -28,9 +28,9 @@
- img - img
- img - img
- text: Think No dynamic Plugins are present, so I will define the requested Host and Client Package. - text: Think No dynamic Plugins are present, so I will define the requested Host and Client Package.
- button "Define Cordis Plugin snapshot noop does nothing, for the snapshot Ready" [expanded]: - button "Register Cordis Plugin snapshot noop does nothing, for the snapshot Ready" [expanded]:
- img - img
- text: Define Cordis Plugin snapshot noop does nothing, for the snapshot Ready - text: Register Cordis Plugin snapshot noop does nothing, for the snapshot Ready
- tablist "Plugin source": - tablist "Plugin source":
- tab "Client" - tab "Client"
- tab "Host" [selected] - tab "Host" [selected]
@@ -66,10 +66,10 @@
- img - img
- img - img
- text: Context injection cordis-host-runner - text: Context injection cordis-host-runner
- button "Stop dynamic package snap-1": - img
- img - text: Stop Cordis Plugin snap-1
- img - button "Inspect"
- text: Stop dynamic package snap-1 - text: Dynamic Plugin snap-1 is stopped; its definition and versions remain.
- paragraph: CORDIS_UI_DONE - paragraph: CORDIS_UI_DONE
- button "Copy": - button "Copy":
- img - img
@@ -62,9 +62,9 @@ const TOOL_VARIANTS: Record<string, ToolRowVariant> = {
const TOOL_TITLES: Record<string, string> = { const TOOL_TITLES: Record<string, string> = {
cordis_package_inspect: 'Inspect', cordis_package_inspect: 'Inspect',
cordis_runtime_inspect: 'Inspect', cordis_runtime_inspect: 'Inspect',
cordis_run: 'Run dynamic package', cordis_run: 'Run Cordis Plugin',
cordis_stop: 'Stop dynamic package', cordis_stop: 'Stop Cordis Plugin',
cordis_undefine: 'Discard dynamic package', cordis_undefine: 'Remove Cordis Plugin',
pwsh: 'Pwsh', pwsh: 'Pwsh',
} }
@@ -228,8 +228,8 @@ describe('run_code sub-calls through the real chat machinery', () => {
// Each run-control verb names its act and shows the package id; without the // Each run-control verb names its act and shows the package id; without the
// owned titles all three would read "Tool call · cordis_run · dyn-2". // owned titles all three would read "Tool call · cordis_run · dyn-2".
expect(nest.querySelector('[data-tool="cordis_runtime_inspect"]')?.textContent).toContain('Inspect') expect(nest.querySelector('[data-tool="cordis_runtime_inspect"]')?.textContent).toContain('Inspect')
expect(nest.querySelector('[data-tool="cordis_run"]')?.textContent).toContain('Run dynamic packagedyn-2') expect(nest.querySelector('[data-tool="cordis_run"]')?.textContent).toContain('Run Cordis Plugindyn-2')
expect(nest.querySelector('[data-tool="cordis_undefine"]')?.textContent).toContain('Discard dynamic packagedyn-2') expect(nest.querySelector('[data-tool="cordis_undefine"]')?.textContent).toContain('Remove Cordis Plugindyn-2')
// None of them is a code row: the program belongs to cordis_define, whose // None of them is a code row: the program belongs to cordis_define, whose
// own keyed card renders it (the next case covers the code row itself). // own keyed card renders it (the next case covers the code row itself).
expect(nest.querySelector('[data-variant="code"]')).toBeNull() expect(nest.querySelector('[data-variant="code"]')).toBeNull()
@@ -53,9 +53,9 @@ describe('tool-call-model', () => {
// Every define/run pair the model makes puts a row in the flow, so the // Every define/run pair the model makes puts a row in the flow, so the
// generic "Tool call · cordis_run · dyn-1" fallback is user-visible slop. // generic "Tool call · cordis_run · dyn-1" fallback is user-visible slop.
const titleOf = (name: string) => toolRowModel(name, running({ name, argsRaw: '{"id":"dyn-1"}' })) const titleOf = (name: string) => toolRowModel(name, running({ name, argsRaw: '{"id":"dyn-1"}' }))
expect(titleOf('cordis_run').title).toBe('Run dynamic package') expect(titleOf('cordis_run').title).toBe('Run Cordis Plugin')
expect(titleOf('cordis_stop').title).toBe('Stop dynamic package') expect(titleOf('cordis_stop').title).toBe('Stop Cordis Plugin')
expect(titleOf('cordis_undefine').title).toBe('Discard dynamic package') expect(titleOf('cordis_undefine').title).toBe('Remove Cordis Plugin')
// An owned title takes the tool name out of the summary slot, leaving the // An owned title takes the tool name out of the summary slot, leaving the
// package id as the only mutable text. // package id as the only mutable text.
expect(titleOf('cordis_run').summary).toBe('dyn-1') expect(titleOf('cordis_run').summary).toBe('dyn-1')
@@ -186,14 +186,14 @@ describe('tool-call-model', () => {
argsRaw: '{"id":"dyn-2"}', argsRaw: '{"id":"dyn-2"}',
}))).toMatchObject({ }))).toMatchObject({
variant: 'others', variant: 'others',
title: 'Run dynamic package', title: 'Run Cordis Plugin',
summary: 'dyn-2', summary: 'dyn-2',
}) })
expect(toolRowModel('cordis_undefine', result({ expect(toolRowModel('cordis_undefine', result({
call: { name: 'cordis_undefine', argsRaw: '{"id":"dyn-2"}' }, call: { name: 'cordis_undefine', argsRaw: '{"id":"dyn-2"}' },
}))).toMatchObject({ }))).toMatchObject({
variant: 'others', variant: 'others',
title: 'Discard dynamic package', title: 'Remove Cordis Plugin',
summary: 'dyn-2', summary: 'dyn-2',
}) })
}) })
@@ -119,9 +119,9 @@ describe('keyed toolview hole through the real machinery', () => {
// generic "Tool call · <name> · <id>" row. // generic "Tool call · <name> · <id>" row.
const rowText = (name: string) => view.container.querySelector(`[data-tool="${name}"]`)?.textContent const rowText = (name: string) => view.container.querySelector(`[data-tool="${name}"]`)?.textContent
expect(rowText('cordis_runtime_inspect')).toContain('Inspect') expect(rowText('cordis_runtime_inspect')).toContain('Inspect')
expect(rowText('cordis_run')).toContain('Run dynamic packagedyn-2') expect(rowText('cordis_run')).toContain('Run Cordis Plugindyn-2')
expect(rowText('cordis_stop')).toContain('Stop dynamic packagedyn-2') expect(rowText('cordis_stop')).toContain('Stop Cordis Plugindyn-2')
expect(rowText('cordis_undefine')).toContain('Discard dynamic packagedyn-2') expect(rowText('cordis_undefine')).toContain('Remove Cordis Plugindyn-2')
// No run-control verb is a code row; the program is cordis_define's, and its // No run-control verb is a code row; the program is cordis_define's, and its
// own keyed card owns that rendering. // own keyed card owns that rendering.
expect(view.container.querySelector('[data-variant="code"]')).toBeNull() expect(view.container.querySelector('[data-variant="code"]')).toBeNull()
@@ -65,7 +65,7 @@ export function presentDefineCall(args: {
return { return {
card: 'generic', card: 'generic',
kind: 'execute', kind: 'execute',
title: `Define Package "${args.name}" for ${target}: ${args.purpose}`, title: `Register Cordis Plugin "${args.name}" for ${target}: ${args.purpose}`,
rawInput: args.code, rawInput: args.code,
} }
} }
@@ -76,7 +76,7 @@ export function presentDefineCall(args: {
* @returns replay-safe generic call presentation. * @returns replay-safe generic call presentation.
*/ */
export function presentUndefineCall(args: { pluginId: string }): GenericCallView { export function presentUndefineCall(args: { pluginId: string }): GenericCallView {
return { card: 'generic', kind: 'delete', title: `Remove dynamic Plugin ${args.pluginId}` } return { card: 'generic', kind: 'delete', title: `Remove Cordis Plugin ${args.pluginId}` }
} }
/** /**
@@ -88,7 +88,7 @@ export function presentRunCall(args: { pluginId: string; packageId: string; mode
return { return {
card: 'generic', card: 'generic',
kind: 'execute', kind: 'execute',
title: `${args.mode === 'update' ? 'Update' : 'Run'} ${args.pluginId} · ${args.packageId}`, title: `${args.mode === 'update' ? 'Update' : 'Run'} Cordis Plugin ${args.pluginId} · ${args.packageId}`,
} }
} }
@@ -98,5 +98,5 @@ export function presentRunCall(args: { pluginId: string; packageId: string; mode
* @returns replay-safe generic call presentation. * @returns replay-safe generic call presentation.
*/ */
export function presentStopCall(args: { pluginId: string }): GenericCallView { export function presentStopCall(args: { pluginId: string }): GenericCallView {
return { card: 'generic', kind: 'execute', title: `Stop dynamic Plugin ${args.pluginId}` } return { card: 'generic', kind: 'execute', title: `Stop Cordis Plugin ${args.pluginId}` }
} }
@@ -0,0 +1,42 @@
/** Localized cards for `cordis_stop` and `cordis_undefine`. */
import {
IconInspectOutline12, IconStopFill16, IconTrashOutline16, StateDot,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import { cordisActionCard } from './card-model.ts'
import css from './CordisRunRow.module.css'
/** Full action-card props composed by the keyed Tool slot. */
export type CordisActionRowProps = ToolCallViewProps & PropsLocale<'cordis'>
/** Render one Stop or Remove call with Cordis-owned localized copy. */
export function CordisActionRow({ callId, toolName, block, inspect, t }: CordisActionRowProps) {
const card = cordisActionCard(block)
const remove = toolName === 'cordis_undefine'
const summary = card.errorSummary ?? card.pluginId ?? callId
return (
<div className={css.card} data-tool={toolName} data-state={card.state}>
<div className={css.row}>
<span className={css.icon}>
{card.state === 'error'
? <StateDot state="error" />
: card.state === 'stopped'
? <StateDot state="warning" />
: remove ? <IconTrashOutline16 size={14} /> : <IconStopFill16 size={14} />}
</span>
<span className={css.title}>{t(remove ? 'row.removeTitle' : 'row.stopTitle')}</span>
<span className={css.separator} aria-hidden />
<span className={card.errorSummary === null ? css.summary : css.error}>{summary}</span>
{inspect !== undefined && (
<button type="button" className={css.inspect} aria-label="Inspect" onClick={inspect}>
<IconInspectOutline12 />
</button>
)}
</div>
{card.output !== null && <pre className={css.output}>{card.output}</pre>}
</div>
)
}
@@ -1,4 +1,4 @@
/** Replay-stable view models for `cordis_define` and `cordis_run` calls. */ /** Replay-stable view models for Cordis lifecycle Tool calls. */
import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' import type { ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client'
import type { import type {
@@ -35,6 +35,14 @@ export interface CordisRunCard {
readonly state: CordisToolState readonly state: CordisToolState
} }
/** Frozen `cordis_stop` or `cordis_undefine` presentation data. */
export interface CordisActionCard {
readonly pluginId: CordisDynamicPluginId | null
readonly output: string | null
readonly errorSummary: string | null
readonly state: CordisToolState
}
function firstLine(text: string): string { function firstLine(text: string): string {
const newline = text.indexOf('\n') const newline = text.indexOf('\n')
return newline === -1 ? text : text.slice(0, newline) return newline === -1 ? text : text.slice(0, newline)
@@ -132,3 +140,22 @@ export function cordisRunCard(block: Block): CordisRunCard {
state, state,
} }
} }
/**
* Derive one Stop or Remove card from its frozen call/result slice.
* @param block - active or settled tool-call block.
* @returns normalized lifecycle-action card fields.
*/
export function cordisActionCard(block: Block): CordisActionCard {
const settled = 'kind' in block
const argsRaw = (settled ? block.call?.argsRaw : block.argsRaw) ?? ''
const args = parseArgs(argsRaw)
const state = stateOf(block)
const output = settled ? resultText(block) : null
return {
pluginId: (args === null ? null : stringAt(args, 'pluginId') ?? stringAt(args, 'id')) as CordisDynamicPluginId | null,
output,
errorSummary: state === 'error' && output !== null ? firstLine(output) : null,
state,
}
}
@@ -7,6 +7,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
import type {} from '@deepseek-ai/dsh-api-remotes/client' import type {} from '@deepseek-ai/dsh-api-remotes/client'
import type { InputTriggerService, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client' import type { InputTriggerService, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type {} from './events.ts' import type {} from './events.ts'
import { CordisActionRow } from './CordisActionRow.tsx'
import { CordisDefineRow } from './CordisDefineRow.tsx' import { CordisDefineRow } from './CordisDefineRow.tsx'
import { CordisRunRow } from './CordisRunRow.tsx' import { CordisRunRow } from './CordisRunRow.tsx'
import { CordisPanel } from './CordisPanel.tsx' import { CordisPanel } from './CordisPanel.tsx'
@@ -19,6 +20,7 @@ import { en, NS, zh } from './locales.ts'
export type { CordisCardFace, CordisPanelFace, CordisRunCardFace, CordisToolViewOwnerProps } from './slots.ts' export type { CordisCardFace, CordisPanelFace, CordisRunCardFace, CordisToolViewOwnerProps } from './slots.ts'
export type { CordisActionResult, CordisDynamicPort, CordisInventoryRow } from './dynamic-port.ts' export type { CordisActionResult, CordisDynamicPort, CordisInventoryRow } from './dynamic-port.ts'
export type { CordisDefineRowProps } from './CordisDefineRow.tsx' export type { CordisDefineRowProps } from './CordisDefineRow.tsx'
export type { CordisActionRowProps } from './CordisActionRow.tsx'
export type { CordisRunRowProps } from './CordisRunRow.tsx' export type { CordisRunRowProps } from './CordisRunRow.tsx'
export type { export type {
CordisRunCardPointer, CordisRunCardStore, CordisToolViewKey, CordisRunCardPointer, CordisRunCardStore, CordisToolViewKey,
@@ -131,6 +133,15 @@ export function apply(ctx: ClientContext): void {
}, },
}, CordisRunRow)) }, CordisRunRow))
ctx.slots.inject('tool.call.toolview', function* () {
yield ctx.slots.register({
name: 'tool.call.toolview', key: 'cordis_stop', locale: NS,
}, CordisActionRow)
yield ctx.slots.register({
name: 'tool.call.toolview', key: 'cordis_undefine', locale: NS,
}, CordisActionRow)
})
const rowsOf = (sessionId: SessionId, query: string) => inventory.getSnapshot().rows const rowsOf = (sessionId: SessionId, query: string) => inventory.getSnapshot().rows
.filter(row => row.agentId === sessionId && String(row.pluginId).includes(query)) .filter(row => row.agentId === sessionId && String(row.pluginId).includes(query))
const source: InputTriggerSource = { const source: InputTriggerSource = {
@@ -7,6 +7,8 @@ export const zh = {
'row.defineTitle': '注册 Cordis 插件', 'row.defineTitle': '注册 Cordis 插件',
'row.runTitle': '运行 Cordis 插件', 'row.runTitle': '运行 Cordis 插件',
'row.updateTitle': '更新 Cordis 插件', 'row.updateTitle': '更新 Cordis 插件',
'row.stopTitle': '停止 Cordis 插件',
'row.removeTitle': '移除 Cordis 插件',
'purpose.missing': '(未填写用途)', 'purpose.missing': '(未填写用途)',
'status.idle': '待激活', 'status.idle': '待激活',
'status.awaitingApproval': '待审批', 'status.awaitingApproval': '待审批',
@@ -65,9 +67,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
/** English Cordis UI messages. */ /** English Cordis UI messages. */
export const en = { export const en = {
'row.defineTitle': 'Define Cordis Plugin', 'row.defineTitle': 'Register Cordis Plugin',
'row.runTitle': 'Run Cordis Plugin', 'row.runTitle': 'Run Cordis Plugin',
'row.updateTitle': 'Update Cordis Plugin', 'row.updateTitle': 'Update Cordis Plugin',
'row.stopTitle': 'Stop Cordis Plugin',
'row.removeTitle': 'Remove Cordis Plugin',
'purpose.missing': '(no purpose given)', 'purpose.missing': '(no purpose given)',
'status.idle': 'Ready', 'status.idle': 'Ready',
'status.awaitingApproval': 'Awaiting approval', 'status.awaitingApproval': 'Awaiting approval',
@@ -3,7 +3,7 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
import { cordisDefineCard } from '../src/client/card-model.ts' import { cordisActionCard, cordisDefineCard } from '../src/client/card-model.ts'
const ARGS = '{"name":"clock","purpose":"顶栏时钟","code":{"client":"return {}","host":"harness.handle(\'now\', () => Date.now())"}}' const ARGS = '{"name":"clock","purpose":"顶栏时钟","code":{"client":"return {}","host":"harness.handle(\'now\', () => Date.now())"}}'
@@ -86,3 +86,20 @@ describe('cordisDefineCard', () => {
expect(card.purpose).toBeNull() expect(card.purpose).toBeNull()
}) })
}) })
describe('cordisActionCard', () => {
it('keeps the Plugin identity and lifecycle result for Stop and Remove cards', () => {
const card = cordisActionCard(settled({
call: { name: 'cordis_stop', argsRaw: '{"pluginId":"clock-1"}' },
content: [{ type: 'text', text: 'Stopped clock-1.' }],
meta: undefined,
}))
expect(card).toEqual({
pluginId: 'clock-1',
output: 'Stopped clock-1.',
errorSummary: null,
state: 'ok',
})
})
})