diff --git a/apps/cli/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md index bea8209c30..08e99271d1 100644 --- a/apps/cli/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md +++ b/apps/cli/config/agent-presets/cordis/skills/cordis-plugin-development/SKILL.md @@ -304,6 +304,7 @@ To customize the call card for an ordinary model Tool, query `tool.call.toolview ### Overlays and local entry points - 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 supplementary content after a conversation turn, query `conversation.chat.turnTail` and register according to its returned chain selector and fallback rules. diff --git a/apps/web/tests/cordis-tool-round.e2e.ts b/apps/web/tests/cordis-tool-round.e2e.ts index a91eaeb16e..a702a7f1ea 100644 --- a/apps/web/tests/cordis-tool-round.e2e.ts +++ b/apps/web/tests/cordis-tool-round.e2e.ts @@ -1,6 +1,6 @@ // 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 -// 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. // // 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) } -describe('web e2e: Cordis tools use the generic row variants', () => { +describe('web e2e: Cordis tools use their owned cards', () => { let scaffold: WebScaffold let browser: Browser let page: Page @@ -129,7 +129,7 @@ describe('web e2e: Cordis tools use the generic row variants', () => { 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')) await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 }) .toBeGreaterThanOrEqual(1) @@ -153,7 +153,7 @@ describe('web e2e: Cordis tools use the generic row variants', () => { await runRow.waitFor({ timeout: 10_000 }) 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 expect.poll(() => stopRow.textContent()).toContain('snap-') await expect(stopRow.getAttribute('data-state')).resolves.toBe('ok') diff --git a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md index e510c79512..0d409c987f 100644 --- a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md +++ b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md @@ -28,9 +28,9 @@ - img - img - 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 - - 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": - tab "Client" - tab "Host" [selected] @@ -66,10 +66,10 @@ - img - img - text: Context injection cordis-host-runner -- button "Stop dynamic package snap-1": - - img - - img - - text: Stop dynamic package snap-1 +- img +- text: Stop Cordis Plugin snap-1 +- button "Inspect" +- text: Dynamic Plugin snap-1 is stopped; its definition and versions remain. - paragraph: CORDIS_UI_DONE - button "Copy": - img diff --git a/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts b/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts index 64067eb000..fa7cb27962 100644 --- a/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/tool-call-model.ts @@ -62,9 +62,9 @@ const TOOL_VARIANTS: Record = { const TOOL_TITLES: Record = { cordis_package_inspect: 'Inspect', cordis_runtime_inspect: 'Inspect', - cordis_run: 'Run dynamic package', - cordis_stop: 'Stop dynamic package', - cordis_undefine: 'Discard dynamic package', + cordis_run: 'Run Cordis Plugin', + cordis_stop: 'Stop Cordis Plugin', + cordis_undefine: 'Remove Cordis Plugin', pwsh: 'Pwsh', } diff --git a/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx b/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx index fe53bfab41..1f0cd29274 100644 --- a/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx +++ b/packages/client/ui-tool/tests/chat-code-subcalls.client.spec.tsx @@ -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 // 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_run"]')?.textContent).toContain('Run dynamic packagedyn-2') - expect(nest.querySelector('[data-tool="cordis_undefine"]')?.textContent).toContain('Discard 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('Remove Cordis Plugindyn-2') // 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). expect(nest.querySelector('[data-variant="code"]')).toBeNull() diff --git a/packages/client/ui-tool/tests/tool-row.client.spec.tsx b/packages/client/ui-tool/tests/tool-row.client.spec.tsx index 3d16405ee3..28c8b5b236 100644 --- a/packages/client/ui-tool/tests/tool-row.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-row.client.spec.tsx @@ -53,9 +53,9 @@ describe('tool-call-model', () => { // 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. const titleOf = (name: string) => toolRowModel(name, running({ name, argsRaw: '{"id":"dyn-1"}' })) - expect(titleOf('cordis_run').title).toBe('Run dynamic package') - expect(titleOf('cordis_stop').title).toBe('Stop dynamic package') - expect(titleOf('cordis_undefine').title).toBe('Discard dynamic package') + expect(titleOf('cordis_run').title).toBe('Run Cordis Plugin') + expect(titleOf('cordis_stop').title).toBe('Stop Cordis Plugin') + expect(titleOf('cordis_undefine').title).toBe('Remove Cordis Plugin') // An owned title takes the tool name out of the summary slot, leaving the // package id as the only mutable text. expect(titleOf('cordis_run').summary).toBe('dyn-1') @@ -186,14 +186,14 @@ describe('tool-call-model', () => { argsRaw: '{"id":"dyn-2"}', }))).toMatchObject({ variant: 'others', - title: 'Run dynamic package', + title: 'Run Cordis Plugin', summary: 'dyn-2', }) expect(toolRowModel('cordis_undefine', result({ call: { name: 'cordis_undefine', argsRaw: '{"id":"dyn-2"}' }, }))).toMatchObject({ variant: 'others', - title: 'Discard dynamic package', + title: 'Remove Cordis Plugin', summary: 'dyn-2', }) }) diff --git a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx index 24e808cf62..12af87cef6 100644 --- a/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx +++ b/packages/client/ui-tool/tests/toolview-slot.client.spec.tsx @@ -119,9 +119,9 @@ describe('keyed toolview hole through the real machinery', () => { // generic "Tool call · · " row. const rowText = (name: string) => view.container.querySelector(`[data-tool="${name}"]`)?.textContent expect(rowText('cordis_runtime_inspect')).toContain('Inspect') - expect(rowText('cordis_run')).toContain('Run dynamic packagedyn-2') - expect(rowText('cordis_stop')).toContain('Stop dynamic packagedyn-2') - expect(rowText('cordis_undefine')).toContain('Discard dynamic packagedyn-2') + expect(rowText('cordis_run')).toContain('Run Cordis Plugindyn-2') + expect(rowText('cordis_stop')).toContain('Stop Cordis Plugindyn-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 // own keyed card owns that rendering. expect(view.container.querySelector('[data-variant="code"]')).toBeNull() diff --git a/packages/extensions/tool-cordis/src/present.ts b/packages/extensions/tool-cordis/src/present.ts index b838d02823..06c91f0dca 100644 --- a/packages/extensions/tool-cordis/src/present.ts +++ b/packages/extensions/tool-cordis/src/present.ts @@ -65,7 +65,7 @@ export function presentDefineCall(args: { return { card: 'generic', kind: 'execute', - title: `Define Package "${args.name}" for ${target}: ${args.purpose}`, + title: `Register Cordis Plugin "${args.name}" for ${target}: ${args.purpose}`, rawInput: args.code, } } @@ -76,7 +76,7 @@ export function presentDefineCall(args: { * @returns replay-safe generic call presentation. */ 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 { card: 'generic', 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. */ 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}` } } diff --git a/packages/extensions/ui-cordis/src/client/CordisActionRow.tsx b/packages/extensions/ui-cordis/src/client/CordisActionRow.tsx new file mode 100644 index 0000000000..2d0e767dd6 --- /dev/null +++ b/packages/extensions/ui-cordis/src/client/CordisActionRow.tsx @@ -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 ( +
+
+ + {card.state === 'error' + ? + : card.state === 'stopped' + ? + : remove ? : } + + {t(remove ? 'row.removeTitle' : 'row.stopTitle')} + + {summary} + {inspect !== undefined && ( + + )} +
+ {card.output !== null &&
{card.output}
} +
+ ) +} diff --git a/packages/extensions/ui-cordis/src/client/card-model.ts b/packages/extensions/ui-cordis/src/client/card-model.ts index 535deca66b..3249e5d49c 100644 --- a/packages/extensions/ui-cordis/src/client/card-model.ts +++ b/packages/extensions/ui-cordis/src/client/card-model.ts @@ -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 { @@ -35,6 +35,14 @@ export interface CordisRunCard { 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 { const newline = text.indexOf('\n') return newline === -1 ? text : text.slice(0, newline) @@ -132,3 +140,22 @@ export function cordisRunCard(block: Block): CordisRunCard { 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, + } +} diff --git a/packages/extensions/ui-cordis/src/client/index.ts b/packages/extensions/ui-cordis/src/client/index.ts index e463178747..a757f08f15 100644 --- a/packages/extensions/ui-cordis/src/client/index.ts +++ b/packages/extensions/ui-cordis/src/client/index.ts @@ -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 { InputTriggerService, InputTriggerSource } from '@deepseek-ai/dsh-client-ui-input-trigger/client' import type {} from './events.ts' +import { CordisActionRow } from './CordisActionRow.tsx' import { CordisDefineRow } from './CordisDefineRow.tsx' import { CordisRunRow } from './CordisRunRow.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 { CordisActionResult, CordisDynamicPort, CordisInventoryRow } from './dynamic-port.ts' export type { CordisDefineRowProps } from './CordisDefineRow.tsx' +export type { CordisActionRowProps } from './CordisActionRow.tsx' export type { CordisRunRowProps } from './CordisRunRow.tsx' export type { CordisRunCardPointer, CordisRunCardStore, CordisToolViewKey, @@ -131,6 +133,15 @@ export function apply(ctx: ClientContext): void { }, }, 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 .filter(row => row.agentId === sessionId && String(row.pluginId).includes(query)) const source: InputTriggerSource = { diff --git a/packages/extensions/ui-cordis/src/client/locales.ts b/packages/extensions/ui-cordis/src/client/locales.ts index e16526b50f..6dc8346b59 100644 --- a/packages/extensions/ui-cordis/src/client/locales.ts +++ b/packages/extensions/ui-cordis/src/client/locales.ts @@ -7,6 +7,8 @@ export const zh = { 'row.defineTitle': '注册 Cordis 插件', 'row.runTitle': '运行 Cordis 插件', 'row.updateTitle': '更新 Cordis 插件', + 'row.stopTitle': '停止 Cordis 插件', + 'row.removeTitle': '移除 Cordis 插件', 'purpose.missing': '(未填写用途)', 'status.idle': '待激活', 'status.awaitingApproval': '待审批', @@ -65,9 +67,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { /** English Cordis UI messages. */ export const en = { - 'row.defineTitle': 'Define Cordis Plugin', + 'row.defineTitle': 'Register Cordis Plugin', 'row.runTitle': 'Run Cordis Plugin', 'row.updateTitle': 'Update Cordis Plugin', + 'row.stopTitle': 'Stop Cordis Plugin', + 'row.removeTitle': 'Remove Cordis Plugin', 'purpose.missing': '(no purpose given)', 'status.idle': 'Ready', 'status.awaitingApproval': 'Awaiting approval', diff --git a/packages/extensions/ui-cordis/tests/card-model.client.spec.ts b/packages/extensions/ui-cordis/tests/card-model.client.spec.ts index aa593aa01d..ec99bd2df2 100644 --- a/packages/extensions/ui-cordis/tests/card-model.client.spec.ts +++ b/packages/extensions/ui-cordis/tests/card-model.client.spec.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' 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())"}}' @@ -86,3 +86,20 @@ describe('cordisDefineCard', () => { 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', + }) + }) +})