feat: show prompt assembly in skills panel

This commit is contained in:
Codex
2026-06-03 08:03:04 +08:00
parent 028730047e
commit 90c5dad065
5 changed files with 190 additions and 2 deletions
+26
View File
@@ -23,6 +23,7 @@ export function createHwlabAgentRunAssemblySummary(options = {}) {
const toolAliases = normalizeResourceToolAliases(options.toolAliases ?? DEFAULT_HWLAB_AGENTRUN_TOOL_ALIASES);
const promptRefs = normalizeResourcePromptRefs(options.promptRefs ?? DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS);
const skillRefs = normalizeResourceSkillRefs(options.skillRefs ?? DEFAULT_HWLAB_AGENTRUN_SKILL_REFS);
const promptAssembly = createPromptAssemblySummary(promptRefs);
const repoUrl = optionalString(options.repoUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_REPO_URL ?? env.AGENTRUN_REPO_URL) ?? DEFAULT_HWLAB_AGENTRUN_REPO_URL;
const branch = optionalString(options.branch ?? env.HWLAB_BOOT_REF ?? env.HWLAB_CODE_AGENT_AGENTRUN_BRANCH ?? env.AGENTRUN_BRANCH) ?? DEFAULT_HWLAB_AGENTRUN_BRANCH;
const commitId = optionalString(options.commitId ?? env.HWLAB_COMMIT_ID ?? env.HWLAB_GIT_SHA ?? env.HWLAB_REVISION);
@@ -47,6 +48,7 @@ export function createHwlabAgentRunAssemblySummary(options = {}) {
},
toolAliases: toolAliases.map((item) => ({ ...item })),
promptRefs: promptRefs.map((item) => ({ ...item })),
promptAssembly,
skillRefs: skillRefs.map((item) => ({ ...item })),
runtimeEnv: {
assembledRuntime: "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME",
@@ -56,6 +58,30 @@ export function createHwlabAgentRunAssemblySummary(options = {}) {
};
}
function createPromptAssemblySummary(promptRefs) {
const refs = promptRefs.map((item) => ({
name: item.name,
path: item.path,
inject: item.inject,
stage: promptInjectStageLabel(item.inject),
required: item.required !== false,
source: "ResourceBundleRef.promptRefs"
}));
return {
status: refs.length > 0 ? "ready" : "missing",
count: refs.length,
requiredCount: refs.filter((item) => item.required).length,
injectStages: [...new Set(refs.map((item) => item.inject))],
refs,
valuesPrinted: false
};
}
function promptInjectStageLabel(inject) {
if (inject === "thread-start") return "thread-start initial prompt";
return inject;
}
export const AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES = Object.freeze([
"AUTH_PASSWORD",
"CODEX_API_KEY",
+12
View File
@@ -61,6 +61,18 @@ test("cloud api skills upload/list/tree/preview keeps preinstalled and uploaded
assert.equal(list.body.agentRunAssembly.resourceBundle.skillsDir, ".agents/skills");
assert.deepEqual(list.body.agentRunAssembly.skillRefs.map((item) => item.name), ["device-pod-cli", "hwlab-agent-runtime"]);
assert.deepEqual(list.body.agentRunAssembly.promptRefs.map((item) => item.name), ["hwlab-v02-runtime"]);
assert.equal(list.body.agentRunAssembly.promptAssembly.status, "ready");
assert.equal(list.body.agentRunAssembly.promptAssembly.valuesPrinted, false);
assert.equal(list.body.agentRunAssembly.promptAssembly.requiredCount, 1);
assert.deepEqual(list.body.agentRunAssembly.promptAssembly.injectStages, ["thread-start"]);
assert.deepEqual(list.body.agentRunAssembly.promptAssembly.refs, [{
name: "hwlab-v02-runtime",
path: "internal/agent/prompts/hwlab-v02-runtime.md",
inject: "thread-start",
stage: "thread-start initial prompt",
required: true,
source: "ResourceBundleRef.promptRefs"
}]);
assert.deepEqual(list.body.agentRunAssembly.toolAliases.map((item) => item.name), ["hwpod", "unidesk-ssh"]);
const sameName = list.body.skills.filter((skill) => skill.name === "same-name");
assert.equal(sameName.length, 2);
+81 -2
View File
@@ -180,6 +180,7 @@ function renderAgentRunAssembly() {
const skillRefs = safeAssemblyItems(assembly.skillRefs);
const toolAliases = safeAssemblyItems(assembly.toolAliases);
const promptRefs = safeAssemblyItems(assembly.promptRefs);
const promptAssembly = safePromptAssembly(assembly.promptAssembly, promptRefs);
const wrap = document.createElement("div");
wrap.className = `skill-assembly-body tone-border-${toneClass(assembly.status || "source")}`;
const head = document.createElement("div");
@@ -200,13 +201,91 @@ function renderAgentRunAssembly() {
const detail = document.createElement("div");
detail.className = "skill-meta";
detail.append(
textSpan(`promptRefs=${promptRefs.map((item) => item.name).join(",") || "none"}`, "mono wrap"),
textSpan(`toolAliases=${toolAliases.map((item) => item.name).join(",") || "none"}`, "mono wrap")
);
wrap.append(head, meta, skills, detail);
wrap.append(head, meta, skills, renderPromptAssembly(promptAssembly), detail);
replaceChildren(el.skillAssembly, wrap);
}
function renderPromptAssembly(promptAssembly) {
const refs = promptAssembly.refs;
const section = document.createElement("div");
section.className = "skill-prompt-assembly";
const head = document.createElement("div");
head.className = "skill-prompt-head";
head.append(textSpan("引导 prompt 装配", "skill-prompt-title"), badge(promptAssembly.status || "unknown", promptAssembly.status || "source"));
const meta = document.createElement("div");
meta.className = "skill-prompt-meta";
meta.append(
textSpan(`refs=${Number(promptAssembly.count) || refs.length}`),
textSpan(`required=${Number(promptAssembly.requiredCount) || refs.filter((item) => item.required).length}`),
textSpan(`inject=${promptAssembly.injectStages.join(",") || "none"}`, "mono wrap")
);
const list = document.createElement("div");
list.className = "skill-prompt-list";
if (refs.length === 0) {
list.append(textSpan("未声明引导 prompt refs。", "skill-prompt-empty"));
} else {
list.append(...refs.map(renderPromptAssemblyRef));
}
section.append(head, meta, list);
return section;
}
function renderPromptAssemblyRef(item) {
const row = document.createElement("div");
row.className = "skill-prompt-row";
const head = document.createElement("div");
head.className = "skill-prompt-row-head";
head.append(textSpan(item.name, "skill-prompt-name mono wrap"), badge(item.required ? "required" : "optional", item.required ? "source" : "pending"));
const meta = document.createElement("div");
meta.className = "skill-prompt-meta";
meta.append(
textSpan(`path=${item.path}`, "mono wrap"),
textSpan(`inject=${item.inject}`, "mono wrap"),
textSpan(`stage=${item.stage}`, "mono wrap"),
textSpan(`source=${item.source}`, "mono wrap")
);
row.append(head, meta);
return row;
}
function safePromptAssembly(promptAssembly, fallbackPromptRefs) {
const fallbackRefs = fallbackPromptRefs.map((item) => ({
...item,
path: String(item.path || "unknown"),
inject: String(item.inject || "thread-start"),
stage: String(item.stage || "thread-start initial prompt"),
required: item.required !== false,
source: String(item.source || "ResourceBundleRef.promptRefs")
}));
if (!promptAssembly || typeof promptAssembly !== "object") {
return createSafePromptAssembly(fallbackRefs);
}
const refs = safeAssemblyItems(promptAssembly.refs).map((item) => ({
...item,
path: String(item.path || "unknown"),
inject: String(item.inject || "thread-start"),
stage: String(item.stage || "thread-start initial prompt"),
required: item.required !== false,
source: String(item.source || "ResourceBundleRef.promptRefs")
}));
return createSafePromptAssembly(refs, promptAssembly);
}
function createSafePromptAssembly(refs, promptAssembly = {}) {
const injectStages = Array.isArray(promptAssembly.injectStages)
? promptAssembly.injectStages.map((item) => String(item)).filter(Boolean)
: [...new Set(refs.map((item) => item.inject).filter(Boolean))];
return {
status: String(promptAssembly.status || (refs.length > 0 ? "ready" : "missing")),
count: Number(promptAssembly.count) || refs.length,
requiredCount: Number(promptAssembly.requiredCount) || refs.filter((item) => item.required).length,
injectStages,
refs
};
}
function safeAssemblyItems(items) {
return Array.isArray(items)
? items.filter((item) => item && typeof item === "object" && item.name).map((item) => ({ ...item, name: String(item.name) }))
+5
View File
@@ -73,6 +73,11 @@ assert.match(app, /function\s+initSkillsPanel\s*\(/u, "missing initSkillsPanel")
assert.match(app, /function\s+loadSkillsSurface\s*\(/u, "missing loadSkillsSurface");
assert.match(app, /function\s+uploadSelectedSkillFiles\s*\(/u, "missing uploadSelectedSkillFiles");
assert.match(app, /function\s+renderSkillPreview\s*\(/u, "missing renderSkillPreview");
assertIncludes(app, "promptAssembly", "skills frontend must render AgentRun prompt assembly summary");
assertIncludes(app, "引导 prompt 装配", "skills frontend must label the prompt assembly panel");
assertIncludes(app, "ResourceBundleRef.promptRefs", "skills frontend must expose prompt assembly source metadata");
assertIncludes(styles, ".skill-prompt-assembly", "skills frontend must style prompt assembly rows");
assertIncludes(styles, ".skill-prompt-row", "skills frontend must style prompt assembly refs");
console.log("hwlab-cloud-web check: skills contracts verified");
for (const term of [
+66
View File
@@ -571,6 +571,72 @@ h3 {
justify-content: space-between;
}
.skill-prompt-assembly {
min-width: 0;
display: grid;
gap: 6px;
padding-top: 7px;
border-top: 1px solid var(--line);
}
.skill-prompt-head,
.skill-prompt-row-head,
.skill-prompt-meta {
min-width: 0;
display: flex;
align-items: center;
gap: 7px;
flex-wrap: wrap;
}
.skill-prompt-head,
.skill-prompt-row-head {
justify-content: space-between;
}
.skill-prompt-list {
min-width: 0;
display: grid;
gap: 0;
}
.skill-prompt-row {
min-width: 0;
display: grid;
gap: 5px;
padding: 7px 0;
border-top: 1px solid var(--line);
}
.skill-prompt-row:first-child {
border-top: 0;
padding-top: 0;
}
.skill-prompt-title,
.skill-prompt-name,
.skill-prompt-empty,
.skill-prompt-meta {
min-width: 0;
overflow-wrap: anywhere;
}
.skill-prompt-title,
.skill-prompt-name {
color: var(--text);
font-weight: 800;
}
.skill-prompt-empty,
.skill-prompt-meta {
color: var(--muted);
font-size: 11px;
}
.skill-prompt-meta {
font-family: var(--mono);
}
.skill-row,
.skill-notice,
.skill-tree-entry {