docs/design/skill-required-capabilities.md
Status: design note; this PR proceeds with Option B and leaves
required-capabilities as a future proposal.
Web Shell can render custom fenced code blocks through its markdown renderer. The
chart renderer proposal uses an echarts-fulldata fenced code block so the model
can return a complete ECharts option and dataset payload that Web Shell renders
as an interactive chart.
That output contract is only useful in clients that can render it. In the CLI, ACP clients, or any other surface without a matching renderer, the same response would appear as a large code block instead of a chart.
The initial bundled chart skill proposal relied on wording to tell the model that the format is for Web Shell. This is a soft guard. If the skill is exposed in a non-Web-Shell session, the model can still choose an output format that the client cannot render.
For the current PR, Qwen Code keeps the renderer extension point in Web Shell
but does not bundle qwencode-viz in core. The Web Shell package includes a
copyable, non-auto-loaded skill template, and hosts should install or inject
that skill only when they also register an echarts-fulldata renderer.
Qwen Code needs a clear way to decide whether a host-specific skill should be shown to the model and to users.
For qwencode-viz, the concrete question is:
required-capabilities skill metadata field?qwencode-viz not be a core bundled skill at all, and instead be
supplied only by Web Shell clients that install or inject it?qwencode-viz as a special case.The codebase already has several visibility controls, but none represent client rendering capabilities:
disable-model-invocation: prevents a skill from being auto-invoked by the
model.user-invocable: controls whether a bundled skill is available as a command.paths: scopes skill availability to matching workspace paths.skills.disabled: disables configured skills.allowedTools: currently used by bundled skill loading to hide cron-oriented
skills when cron tools are unavailable.supportedModes: filters commands by execution mode.There is no existing required-capabilities or equivalent skill frontmatter.
Adding it would be a new skill contract.
required-capabilitiesAdd a generic skill frontmatter field:
---
name: qwencode-viz
description: Render analytical charts in Web Shell using echarts-fulldata fenced code blocks.
required-capabilities:
- markdown.codeBlock.echarts-fulldata
---
When the current client/session does not advertise all listed capabilities, the skill is treated as unavailable.
Use namespaced string capabilities:
markdown.codeBlock.echarts-fulldata
This keeps the field generic while making the contract precise:
markdown: the capability belongs to rendered markdown.codeBlock: the capability applies to fenced code block rendering.echarts-fulldata: the specific language/info string supported by the
renderer.Future examples could be:
markdown.codeBlock.vega-litemarkdown.codeBlock.mermaid-interactiveartifact.openUrlAdd requiredCapabilities?: string[] to skill configuration after parsing the
frontmatter key required-capabilities.
Both skill parsing paths should understand the field:
packages/core/src/skills/skill-load.tspackages/core/src/skills/skill-manager.tsThe field should be optional. Missing or empty means the skill has no client capability requirement.
Add client/session capabilities to the runtime config:
interface ConfigParameters {
clientCapabilitiesProvider?: () => ReadonlySet<string>;
}
Expose a helper on Config, for example:
config.getClientCapabilities(): ReadonlySet<string>
Then centralize the check:
function skillMeetsRequiredCapabilities(skill: Skill, config: Config): boolean {
return skill.config.requiredCapabilities.every((capability) =>
config.getClientCapabilities().has(capability),
);
}
The capability filter should be applied before skills are exposed to either the model or the user:
collectAvailableSkillEntries in packages/core/src/tools/skill-utils.ts
should skip skills whose required capabilities are missing. This keeps startup
skill reminders, delta reminders, SkillTool validation, and model-invocable
activation aligned.BundledSkillLoader should skip unavailable bundled skills when creating
user-facing commands.SkillCommandLoader should skip unavailable file-system skills when creating
user-facing commands.The important invariant is that a skill hidden from the model should not still appear as an invocable command unless the project intentionally supports a manual override.
Web Shell should advertise renderer support explicitly rather than relying on
the presence of an opaque renderCodeBlock callback.
For example:
<WebShell
customization={{
markdown: {
renderableCodeBlockLanguages: ['echarts-fulldata'],
renderCodeBlock(info) {
// render custom blocks
},
},
}}
/>
The Web Shell client can map that to:
markdown.codeBlock.echarts-fulldata
This makes the capability declaration stable even if the renderer callback contains custom logic, fallbacks, or multiple supported languages.
For hosted or daemon-based sessions, the client capability set needs to reach core before skills are loaded or listed. A minimal version can pass capabilities when creating a session:
interface CreateSessionRequest {
clientCapabilities?: string[];
}
The daemon bridge, SDK, and ACP session creation flow can store this as session-scoped config.
For the first version, capabilities can be session-scoped. If multiple clients attach to the same session, the behavior should be documented as using the capabilities from session creation time.
qwencode-viz as one canonical bundled skill.qwencode-viz is the only expected
capability-gated skill.Do not add a generic required-capabilities field. Instead, avoid bundling
qwencode-viz in core. The Web Shell client, or any client that supports the
renderer, supplies the skill itself.
Possible distribution models:
.qwen/skills/qwencode-viz/SKILL.md.In this model, the skill is available only because the rendering client chose to provide it.
A Web Shell host that wants chart output should opt in to both halves of the contract:
echarts-fulldata Markdown code block renderer.packages/web-shell/docs/examples/qwencode-viz/SKILL.md.For example:
import * as echarts from 'echarts';
import {
WebShellWithProviders,
createEchartsFullDataRenderer,
} from '@qwen-code/web-shell';
<WebShellWithProviders
baseUrl="http://127.0.0.1:4170"
token={token}
sessionId={sessionId}
markdown={{
renderCodeBlock: createEchartsFullDataRenderer({
loadEcharts: () => echarts,
resolveDataRef: async (ref, meta) =>
loadControlledChartDataset(ref, meta),
}),
}}
/>;
In this renderer configuration, loadEcharts lets the host provide the
approved ECharts runtime, either as a static import or a lazy-loaded module.
resolveDataRef is only used for data.kind="ref" chart blocks; it is the
host-owned bridge from a model-visible data reference to a trusted dataset.
The model-facing envelope format is described by the optional skill template in
packages/web-shell/docs/examples/qwencode-viz/SKILL.md; the renderer-side
validation lives in
packages/web-shell/client/components/messages/EchartsFullDataBlock.tsx.
The skill file should be installed or injected only by hosts that perform this registration. A simple file-based integration can copy:
packages/web-shell/docs/examples/qwencode-viz/SKILL.md
to the workspace or user skill directory, for example:
.qwen/skills/qwencode-viz/SKILL.md
An integration with its own skill distribution layer can instead load the same file as the canonical source content and expose it through that layer. In both cases, core does not auto-load the skill; the host owns enabling it because the host owns the renderer.
For data.kind="ref" envelopes, the built-in renderer validates that data.ref
uses a normalized artifact:// or session-file:// reference before it calls
the host-controlled resolveDataRef(ref, meta) implementation. The renderer
also parses the block as JSON and sanitizes the ECharts option before rendering;
it does not evaluate model-provided JavaScript, fetch arbitrary URLs, or read
local files by itself. A custom renderer should preserve the same split:
renderer-level JSON/ref/option validation first, host-owned artifact resolution
second.
A daemon-backed host can treat the workspace file API as one artifact backend.
For example, the host can persist chart artifacts under a controlled workspace
directory such as .qwen/artifacts/, expose model-facing references like
artifact://chart-data/orders.csv, and resolve them through daemon
GET /file?path=.qwen/artifacts/chart-data/orders.csv. This keeps
artifact:// as the public chart contract while allowing the first
implementation to reuse daemon workspace files.
The resolver must still enforce the artifact root before calling the daemon:
const ARTIFACT_ROOT = '.qwen/artifacts/';
const MAX_CHART_DATA_BYTES = 256 * 1024;
async function resolveDataRef(
ref: string,
meta: { format?: string; dimensions?: string[] },
) {
const artifactPrefix = 'artifact://';
if (!ref.startsWith(artifactPrefix)) {
throw new Error(`Unsupported chart data ref: ${ref}`);
}
const artifactPath = ref.slice(artifactPrefix.length);
if (
artifactPath.length === 0 ||
artifactPath.startsWith('/') ||
artifactPath.includes('\\') ||
artifactPath.split('/').includes('..')
) {
throw new Error(`Invalid chart data ref: ${ref}`);
}
const url = new URL('/file', daemonBaseUrl);
url.searchParams.set('path', `${ARTIFACT_ROOT}${artifactPath}`);
url.searchParams.set('maxBytes', String(MAX_CHART_DATA_BYTES));
const response = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
});
if (!response.ok) {
throw new Error(`Failed to read chart data: ${response.status}`);
}
const file = (await response.json()) as { content: string };
return meta.format === 'csv'
? parseCsvAsArrayRows(file.content, meta.dimensions)
: JSON.parse(file.content);
}
This example intentionally maps only normalized artifact:// paths under
.qwen/artifacts/. If a host later moves artifacts to object storage or a
session-scoped artifact service, only resolveDataRef needs to change; the
model-facing echarts-fulldata block can keep using the same ref shape.
For this PR, use Option B.
That keeps the core skill system unchanged and avoids exposing
echarts-fulldata instructions in unsupported clients. The Web Shell renderer
hook remains useful for any host-owned block renderer, while chart-specific
model instructions become an explicit host opt-in.
Longer term, discuss this as a product/API boundary decision.
Choose Option A if maintainers expect Qwen Code to support more client-rendered
output contracts over time. In that case, required-capabilities is a small
general contract that keeps skill exposure honest across CLI, Web Shell, ACP,
and future clients.
Choose Option B if qwencode-viz is expected to remain a Web-Shell-only
extension and maintainers do not want core skills to depend on client rendering
features. In that case, the current bundled skill should be removed from core
and supplied by Web Shell clients that support echarts-fulldata.
The recommended future default is Option A only if maintainers are comfortable making client/session capabilities part of the skill system. Otherwise, keep host-renderer skills client-owned.
/skills, or shown as
disabled with a reason?echarts-fulldata blocks in unsupported clients?required-capabilities, requires-capabilities, or
client-capabilities?If Option A is implemented, add tests for:
collectAvailableSkillEntries hiding a skill when capabilities are missing.paths, skills.disabled, and disable-model-invocation.BundledSkillLoader and SkillCommandLoader command visibility.required-capabilities are unchanged.Existing skills require no migration because the new field is optional.
For the current Option B path, remove the chart skill from core bundled skills. The Web Shell package template must not be loaded by core automatically; hosts opt in by installing or injecting it.
If Option A is accepted, add:
required-capabilities:
- markdown.codeBlock.echarts-fulldata
to a future bundled qwencode-viz.
If Option B is accepted, remove the chart skill from core bundled skills and
document how Web Shell clients can install or inject it when they register an
echarts-fulldata renderer.