用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/csharpfritz/SquadUI --skill vscode-dashboard-webviews命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
基于 SOC 职业分类
| name | vscode-dashboard-webviews |
| description | Patterns for building dashboard webviews with tabs and visualizations |
| domain | vscode-extension-ui |
| confidence | high |
| source | manual |
VS Code dashboard webviews require specific patterns for tab navigation, data visualization, and lifecycle management. This skill captures the architecture established for SquadUI's dashboard feature.
Use a single webview panel that hosts multiple tabs rather than separate webviews for related features:
export class SquadDashboardWebview {
private panel: vscode.WebviewPanel | undefined;
constructor(extensionUri: vscode.Uri, dataProvider: DataProvider) {
this.extensionUri = extensionUri;
this.dataProvider = dataProvider;
}
async show(): Promise<void> {
if (this.panel) {
this.panel.reveal(vscode.ViewColumn.One);
await this.updateContent();
} else {
this.createPanel();
await this.updateContent();
}
}
}
Why: Reduces activation cost, natural grouping for related insights, avoids panel proliferation.
this.panel = vscode.window.createWebviewPanel(
'squadui.dashboard',
'Squad Dashboard',
vscode.ViewColumn.One,
{
enableScripts: true, // Required for tab navigation JS
retainContextWhenHidden: true, // Preserve tab state
localResourceRoots: [this.extensionUri],
}
);
Separate concerns: Service → Builder → Template
OrchestrationLogService + DataProvider
↓
DashboardDataBuilder (transforms raw logs → chart data)
↓
htmlTemplate.ts (renders HTML with embedded data)
Builder Pattern:
export class DashboardDataBuilder {
buildDashboardData(
logEntries: LogEntry[],
members: Member[],
tasks: Task[]
): DashboardData {
return {
velocity: {
timeline: this.buildVelocityTimeline(tasks),
heatmap: this.buildActivityHeatmap(members, logEntries),
},
activity: { /* ... */ },
decisions: { /* ... */ },
};
}
}
Avoid chart libraries. Use HTML5 Canvas + CSS Grid for simple visualizations:
// Line chart with Canvas
function renderVelocityChart() {
const canvas = document.getElementById('velocity-chart');
const ctx = canvas.getContext('2d');
// Draw axes
ctx.strokeStyle = 'var(--vscode-panel-border)';
ctx.moveTo(padding, padding);
ctx.lineTo(padding, padding + height);
ctx.stroke();
// Draw line
ctx.strokeStyle = 'var(--vscode-charts-blue)';
timeline.forEach((point, i) => {
const x = padding + i * stepX;
const y = padding + height - (point.value / maxValue) * height;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
// Heatmap with CSS Grid
<div class="heatmap-grid">
${heatmap.map(point => `
<div class="heatmap-cell">
<div class="member-name">${point.member}</div>
<div class="activity-bar">
<div class="activity-fill" style="width: ${point.activityLevel * 100}%"></div>
</div>
</div>
`).join('')}
</div>
Use color coding and borders to indicate state at-a-glance:
.task-item.done {
background-color: rgba(40, 167, 69, 0.15);
border-left: 3px solid var(--vscode-charts-green);
}
.task-item.in-progress {
background-color: rgba(255, 193, 7, 0.15);
border-left: 3px solid var(--vscode-charts-orange);
}
const isDone = task.endDate !== null;
const statusClass = isDone ? 'done' : 'in-progress';
tasksHtml += `<li class="task-item ${statusClass}">...</li>`;
Color Palette:
var(--vscode-charts-green))var(--vscode-charts-orange))var(--vscode-charts-blue))Pure CSS tooltips for hover details (no JS needed):
.task-item .tooltip {
visibility: hidden;
position: absolute;
z-index: 1000;
bottom: 125%;
left: 12px;
min-width: 250px;
background-color: var(--vscode-editorWidget-background);
border: 1px solid var(--vscode-editorWidget-border);
padding: 10px;
opacity: 0;
transition: opacity 0.2s;
}
.task-item:hover .tooltip {
visibility: visible;
opacity: 1;
}
<li class="task-item" title="Task name">
<span>Task name</span>
<div class="tooltip">
<div class="tooltip-title">Task name</div>
<div class="tooltip-meta">Status: Completed</div>
</div>
</li>
Why CSS tooltips: Lighter than JS-based solutions, no postMessage overhead, theme-aware via CSS variables.
Use vanilla JS for tab switching — no frameworks needed:
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
const targetTab = tab.dataset.tab;
// Update buttons
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Update content
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(targetTab + '-tab').classList.add('active');
});
});
Serialize data as JSON in HTML template:
export function getDashboardHtml(data: DashboardData): string {
const velocityDataJson = JSON.stringify(data.velocity);
return `
<script>
const velocityData = ${velocityDataJson};
renderVelocityChart();
</script>
`;
}
Security: CSP allows script-src 'unsafe-inline' for inline scripts. Data is server-controlled, not user input.
src/views/
SquadDashboardWebview.ts ← Main webview class
dashboard/ ← Dashboard-specific logic
DashboardDataBuilder.ts ← Data transformation
htmlTemplate.ts ← HTML generation
Wire status bar to open dashboard:
this.statusBarItem.command = 'squadui.openDashboard';
User clicks status bar → dashboard opens → sees visualizations.
buildActivityHeatmap(members: Member[], logs: LogEntry[]): HeatmapPoint[] {
const participationCount = new Map<string, number>();
let maxParticipation = 0;
for (const entry of logs) {
for (const participant of entry.participants) {
const count = (participationCount.get(participant) ?? 0) + 1;
participationCount.set(participant, count);
maxParticipation = Math.max(maxParticipation, count);
}
}
// Normalize to 0.0-1.0 scale
return members.map(member => ({
member: member.name,
activityLevel: maxParticipation > 0
? (participationCount.get(member.name) ?? 0) / maxParticipation
: 0,
}));
}
buildVelocityTimeline(tasks: Task[]): DataPoint[] {
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const tasksByDate = new Map<string, number>();
for (const task of tasks.filter(t => t.status === 'completed')) {
const dateKey = task.completedAt.toISOString().split('T')[0];
tasksByDate.set(dateKey, (tasksByDate.get(dateKey) ?? 0) + 1);
}
// Fill in missing dates with 0 counts
const timeline: DataPoint[] = [];
for (let d = new Date(thirtyDaysAgo); d <= now; d.setDate(d.getDate() + 1)) {
const dateKey = d.().()[];
timeline.({
: dateKey,
: tasksByDate.(dateKey) ?? ,
});
}
timeline;
}
retainContextWhenHidden — Tabs lose state when user switches away.display: none containers return offsetWidth === 0. Setting canvas.width = canvas.offsetWidth produces a zero-width bitmap. Always defer canvas rendering until the tab is visible. Add offsetWidth === 0 guards as safety nets.Set, flag) to attach listeners only once. Otherwise each call stacks duplicate handlers.