From 34dc1afb51ddab8cbf190d022ebaf12e8b5bcddf Mon Sep 17 00:00:00 2001 From: Jonas Kunert Date: Tue, 21 Jul 2026 18:27:29 +0200 Subject: [PATCH 1/2] Add Plan Files command to open/edit ~/.claude/plans/*.md (#9) Adds a "Show Plan Files" command and a header button that list plan markdown files written by the Claude CLI, open the newest/only one directly or offer a QuickPick for multiple, reusing the existing file editor helper. Saving a plan file from the editor now offers to notify the running Claude session via the existing send-message path, but only shows the info message (no button) if no webview is active. Co-Authored-By: Claude Fable 5 --- package.json | 8 ++++ src/extension.ts | 102 ++++++++++++++++++++++++++++++++++++++++++++++- src/script.ts | 6 +++ src/ui.ts | 1 + 4 files changed, 116 insertions(+), 1 deletion(-) 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..5aaa31b 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,94 @@ 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 && 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 === 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 + From cc493130f378e579d8247521b0a70e9013f61be1 Mon Sep 17 00:00:00 2001 From: Jonas Kunert Date: Tue, 21 Jul 2026 18:32:15 +0200 Subject: [PATCH 2/2] Fix plan file listing: symlink filter and empty-stat edge case (#9) FileType is a bitmask, so a symlinked .md (File|SymbolicLink) was skipped by the strict equality check against FileType.File; use a bit test instead. Also guard against every stat() call failing after readDirectory succeeded (e.g. file removed in between), which left showQuickPick called with an empty list instead of showing the "no plan files" message. Co-Authored-By: Claude Fable 5 --- src/extension.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 5aaa31b..5fd8d38 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3896,7 +3896,7 @@ class ClaudeChatProvider { return; } - const mdFiles = entries.filter(([name, type]) => type === vscode.FileType.File && name.toLowerCase().endsWith('.md')); + 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; @@ -3912,6 +3912,11 @@ class ClaudeChatProvider { } 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;