Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ class ClaudeChatProvider {
private _accountInfoFetchedThisSession: boolean = false; // Track if we fetched account info this session
private _pendingModelAfterPayment: string | null = null;
private _currentSessionId: string | undefined;
// Reported by the CLI's system/init message; covers plugins loaded via --plugin-dir
private _pluginSlashCommands: string[] | undefined;
private _pluginSkillIds: string[] | undefined;
private _loadedPlugins: Array<{ name: string; path: string }> | undefined;
private _backupRepoPath: string | undefined;
private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = [];
private _conversationsPath: string | undefined;
Expand Down Expand Up @@ -1316,12 +1320,29 @@ class ClaudeChatProvider {
//this._sendAndSaveMessage({ type: 'init', data: { sessionId: jsonData.session_id; } })

// Show session info in UI
// Skills and commands contributed by loaded plugins (including --plugin-dir).
// Cached so the picker is populated before the first message of a session.
this._pluginSlashCommands = jsonData.slash_commands || [];
this._pluginSkillIds = jsonData.skills || [];
this._loadedPlugins = jsonData.plugins || [];
try {
this._context.globalState.update('claude.pluginCommands', {
slashCommands: this._pluginSlashCommands,
skills: this._pluginSkillIds,
plugins: this._loadedPlugins
});
} catch (error) {
console.error('Error caching plugin commands:', error);
}

this._sendAndSaveMessage({
type: 'sessionInfo',
data: {
sessionId: jsonData.session_id,
tools: jsonData.tools || [],
mcpServers: jsonData.mcp_servers || []
mcpServers: jsonData.mcp_servers || [],
slashCommands: this._pluginSlashCommands,
plugins: this._loadedPlugins
}
});
} else if (jsonData.subtype === 'status') {
Expand Down Expand Up @@ -2572,6 +2593,49 @@ class ClaudeChatProvider {
} catch { /* dir doesn't exist */ }
}

// Merge in skills contributed by loaded plugins. These live under the plugin's
// own directory, not ~/.claude/skills, so the scans above never see them.
try {
const cached = this._context.globalState.get<any>('claude.pluginCommands') || {};
const plugins = this._loadedPlugins || cached.plugins || [];
const skillIds = this._pluginSkillIds || cached.skills || [];
const pluginPath: { [name: string]: string } = {};
for (const plugin of plugins) {
if (plugin && plugin.name && plugin.path) {
pluginPath[plugin.name] = plugin.path;
}
}
for (const id of skillIds) {
const sep = typeof id === 'string' ? id.indexOf(':') : -1;
if (sep <= 0) {
continue; // built-in skill, not plugin-provided
}
if (skills.some((s: any) => s.name === id)) {
continue;
}
const base = pluginPath[id.slice(0, sep)];
if (!base) {
continue;
}
let description = '';
let body = '';
try {
const skillMd = path.join(base, 'skills', id.slice(sep + 1), 'SKILL.md');
const raw = await vscode.workspace.fs.readFile(vscode.Uri.file(skillMd));
const text = new TextDecoder().decode(raw);
const descMatch = text.match(/description:\s*(.+)/);
const bodyMatch = text.match(/^---[\s\S]*?---\s*([\s\S]*)$/);
description = descMatch ? descMatch[1].trim().replace(/^["']|["']$/g, '') : '';
body = bodyMatch ? bodyMatch[1].trim() : text;
} catch {
/* plugin lays its skills out differently; still list the skill */
}
skills.push({ name: id, scope: 'plugin', description, content: body });
}
} catch (error) {
console.error('Error merging plugin skills:', error);
}

this._postMessage({ type: 'skillsList', data: skills });
}

Expand Down Expand Up @@ -2881,6 +2945,14 @@ class ClaudeChatProvider {
type: 'customSnippetsData',
data: customSnippets
});

// Replay cached plugin commands: the webview asks for snippets on load,
// which is before any session has emitted system/init.
const cached = this._context.globalState.get<any>('claude.pluginCommands') || {};
const pluginCommands = this._pluginSlashCommands || cached.slashCommands || [];
if (pluginCommands.length) {
this._postMessage({ type: 'pluginCommands', data: pluginCommands });
}
} catch (error) {
console.error('Error loading custom snippets:', error);
this._postMessage({
Expand Down
41 changes: 41 additions & 0 deletions src/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3235,6 +3235,40 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt
}
}

function usePluginCommand(name) {
hideSlashCommandsModal();
messageInput.value = '/' + name + ' ';
messageInput.focus();
autoResizeTextarea();
}

function renderPluginCommands(commands) {
var list = document.getElementById('promptSnippetsList');
if (!list) { return; }
var stale = list.querySelectorAll('.plugin-command-item');
for (var i = 0; i < stale.length; i++) { stale[i].remove(); }
if (!commands || !commands.length) { return; }
var names = [];
for (var j = 0; j < commands.length; j++) {
var c = commands[j];
if (typeof c === 'string' && c.indexOf(':') > 0) { names.push(c); }
}
names.sort();
for (var k = 0; k < names.length; k++) {
var name = names[k];
var el = document.createElement('div');
el.className = 'slash-command-item prompt-snippet-item plugin-command-item';
el.onclick = (function (n) { return function () { usePluginCommand(n); }; })(name);
el.innerHTML = '<div class="slash-command-icon">&#128268;</div>' +
'<div class="slash-command-content">' +
'<div class="slash-command-title">/' + escapeHtml(name) + '</div>' +
'<div class="slash-command-description">Plugin skill from ' +
escapeHtml(name.split(':')[0]) + '</div>' +
'</div>';
list.appendChild(el);
}
}

function showAddSnippetForm() {
document.getElementById('addSnippetForm').style.display = 'block';
document.getElementById('snippetName').focus();
Expand Down Expand Up @@ -3663,6 +3697,9 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt
break;

case 'sessionInfo':
if (message.data.slashCommands) {
renderPluginCommands(message.data.slashCommands);
}
if (message.data.sessionId) {
showSessionInfo(message.data.sessionId);
// Show detailed session information
Expand Down Expand Up @@ -5142,6 +5179,10 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt
window.addEventListener('message', event => {
const message = event.data;

if (message.type === 'pluginCommands') {
renderPluginCommands(message.data || []);
return;
}
if (message.type === 'customSnippetsData') {
// Update global custom snippets data
customSnippetsData = message.data || {};
Expand Down
2 changes: 1 addition & 1 deletion src/skills-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const getSkillsScript = () => `
'</div>' +
'<div class="server-actions" style="flex-shrink:0;">' +
'<button class="btn outlined" style="font-size:11px;padding:3px 8px;" onclick="toggleSkillDetail(\\'' + detailId + '\\')">Details</button>' +
'<button class="btn outlined server-delete-btn" data-skill="' + escapeHtml(skill.name) + '" data-scope="' + escapeHtml(skill.scope) + '" onclick="deleteSkill(this.dataset.skill, this.dataset.scope)">Delete</button>' +
(skill.scope === 'plugin' ? '' : '<button class="btn outlined server-delete-btn" data-skill="' + escapeHtml(skill.name) + '" data-scope="' + escapeHtml(skill.scope) + '" onclick="deleteSkill(this.dataset.skill, this.dataset.scope)">Delete</button>') +
'</div>' +
'</div>' +
'<div id="' + detailId + '" class="skill-detail-content" style="display:none;">' +
Expand Down