diff --git a/package.json b/package.json index 6bcdf89..74d5ee4 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,11 @@ "title": "Open Claude Code Chat", "category": "Claude Code Chat", "icon": "icon-bubble.png" + }, + { + "command": "claude-code-chat.showPlan", + "title": "Show Plan Files", + "category": "Claude Code Chat" } ], "keybindings": [ @@ -73,6 +78,9 @@ "commandPalette": [ { "command": "claude-code-chat.openChat" + }, + { + "command": "claude-code-chat.showPlan" } ], "editor/context": [ diff --git a/src/extension.ts b/src/extension.ts index 8fa37fb..5fd8d38 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -44,6 +44,15 @@ export function activate(context: vscode.ExtensionContext) { provider.loadConversation(filename); }); + const showPlanDisposable = vscode.commands.registerCommand('claude-code-chat.showPlan', () => { + provider.showPlanFiles(); + }); + + // Offer to notify Claude when a plan file (~/.claude/plans/*.md) is saved from the editor + const planFileSaveDisposable = vscode.workspace.onDidSaveTextDocument(document => { + provider.handlePlanFileSaved(document); + }); + // Register webview view provider for sidebar chat (using shared provider instance) const webviewProvider = new ClaudeChatWebviewProvider(context.extensionUri, provider); vscode.window.registerWebviewViewProvider('claude-code-chat.chat', webviewProvider); @@ -98,7 +107,7 @@ export function activate(context: vscode.ExtensionContext) { } }); - context.subscriptions.push(disposable, loadConversationDisposable, configChangeDisposable, statusBarItem, uriHandler); + context.subscriptions.push(disposable, loadConversationDisposable, showPlanDisposable, planFileSaveDisposable, configChangeDisposable, statusBarItem, uriHandler); } export function deactivate() { @@ -595,6 +604,9 @@ class ClaudeChatProvider { case 'openFile': this._openFileInEditor(message.filePath); return; + case 'showPlanFiles': + this.showPlanFiles(); + return; case 'openDiff': this._openDiffEditor(message.oldContent, message.newContent, message.filePath); return; @@ -3866,6 +3878,99 @@ class ClaudeChatProvider { this._context.globalState.update('wslAlertDismissed', true); } + // ─── Plan Files ─── + + private _getPlansDir(): string { + const homeDir = process.env.HOME || process.env.USERPROFILE || ''; + return path.join(homeDir, '.claude', 'plans'); + } + + public async showPlanFiles(): Promise { + const plansDir = this._getPlansDir(); + + let entries: [string, vscode.FileType][]; + try { + entries = await vscode.workspace.fs.readDirectory(vscode.Uri.file(plansDir)); + } catch { + vscode.window.showInformationMessage('No plan files found in ~/.claude/plans/'); + return; + } + + const mdFiles = entries.filter(([name, type]) => (type & vscode.FileType.File) !== 0 && name.toLowerCase().endsWith('.md')); + if (mdFiles.length === 0) { + vscode.window.showInformationMessage('No plan files found in ~/.claude/plans/'); + return; + } + + const plans: { name: string; filePath: string; mtime: number }[] = []; + for (const [name] of mdFiles) { + const filePath = path.join(plansDir, name); + try { + const stat = await vscode.workspace.fs.stat(vscode.Uri.file(filePath)); + plans.push({ name, filePath, mtime: stat.mtime }); + } catch { /* stat failed, skip */ } + } + plans.sort((a, b) => b.mtime - a.mtime); + + if (plans.length === 0) { + vscode.window.showInformationMessage('No plan files found in ~/.claude/plans/'); + return; + } + + if (plans.length === 1) { + this._openFileInEditor(plans[0].filePath); + return; + } + + const picked = await vscode.window.showQuickPick( + plans.map(plan => ({ + label: plan.name.replace(/\.md$/i, ''), + description: new Date(plan.mtime).toLocaleString(), + filePath: plan.filePath + })), + { placeHolder: 'Select a plan file to open' } + ); + + if (picked) { + this._openFileInEditor(picked.filePath); + } + } + + public async handlePlanFileSaved(document: vscode.TextDocument): Promise { + const savedPath = document.uri.fsPath; + if (!savedPath.toLowerCase().endsWith('.md')) { + return; + } + + const plansDir = path.resolve(this._getPlansDir()); + const resolvedSavedPath = path.resolve(savedPath); + const isWithinPlansDir = process.platform === 'win32' + ? resolvedSavedPath.toLowerCase().startsWith(plansDir.toLowerCase() + path.sep) + : resolvedSavedPath.startsWith(plansDir + path.sep); + + if (!isWithinPlansDir) { + return; + } + + const fileName = path.basename(savedPath, '.md'); + const hasActiveWebview = !!(this._panel || this._webview); + + if (!hasActiveWebview) { + vscode.window.showInformationMessage(`Plan file "${fileName}" saved.`); + return; + } + + const selection = await vscode.window.showInformationMessage( + `Plan file "${fileName}" saved. Tell Claude about your changes?`, + 'Send to Claude', + 'Dismiss' + ); + + if (selection === 'Send to Claude') { + this._sendMessageToClaude(`I updated the plan file "${fileName}" in ~/.claude/plans/. Please re-read it and adjust your approach accordingly.`); + } + } + private async _openFileInEditor(filePath: string) { try { const uri = vscode.Uri.file(filePath); diff --git a/src/script.ts b/src/script.ts index 4c949e2..5548860 100644 --- a/src/script.ts +++ b/src/script.ts @@ -4319,6 +4319,12 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt }); } + function showPlanFiles() { + vscode.postMessage({ + type: 'showPlanFiles' + }); + } + function restoreToCommit(commitSha) { vscode.postMessage({ type: 'restoreCommit', diff --git a/src/ui.ts b/src/ui.ts index 4ffcab7..93995b1 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -31,6 +31,7 @@ const getHtml = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'https +