Skip to content

Commit e40ffed

Browse files
jsonifyclaude
andauthored
Build user story generator from brief descriptions (#91)
* Add User Story built-in template - Add 'user-story' template with title, description, tasks, acceptance criteria, and time estimate - Include template metadata in picker with description - Add User Stories category with 📝 icon for organization - Support auto-categorization with 'user-story' and 'story' keywords - Template includes placeholders for {filename}, {date}, and {user} * Add AI-enhanced user story creation command - Add 'noted.createUserStoryWithAI' command to create user stories from brief descriptions - Uses VS Code LLM API (GitHub Copilot) to analyze description and populate: - Story title - User story format (As a... I want... So that...) - 3-5 actionable tasks as checklist - 3-5 testable acceptance criteria - Realistic time estimate - Creates note in User Stories folder with YAML frontmatter - Supports fallback when AI is unavailable - Register command in extension.ts and package.json - Icon: $(notebook) for command palette * Fix user story creation and add to templates picker Issue 1 - Fix workspace folder error: - Use getNotesPath() from configService instead of workspace folders - Support absolute path configurations for notes folder - Add getNotesPath import to templateCommands.ts - Provide clearer error message when notes folder not configured Issue 2 - Add AI User Story to templates picker: - Add "$(sparkle) User Story with AI" option at top of template picker - Shows as "AI-Enhanced" category in picker UI - Accessible via Cmd+K Cmd+N template selection flow - Special value '__ai_user_story__' triggers handleCreateUserStoryWithAI() - Also added regular "User Story" template to built-in list * Default AI model selection to latest Claude Sonnet Update model selection priority in both TemplateGenerator and user story creation: Priority hierarchy: 1. Claude Sonnet (latest version) - best for structured JSON output 2. Claude Opus - most capable 3. Other Claude models 4. Direct Anthropic vendor 5. GPT-4 models 6. Other GPT models 7. Gemini models 8. First available model Key improvements: - Filters for all Sonnet models and sorts by ID to get latest (e.g., sonnet-4-5) - Case-insensitive matching for model family/ID - Checks both 'copilot' and 'anthropic' vendors for Claude models - Maintains user preference override at top of hierarchy - Provides clear fallback chain for reliability Applies to: - User story AI generation (templateCommands.ts) - Template creation with AI (TemplateGenerator.ts) * Add User Story templates to Templates panel Add both manual and AI-enhanced User Story options to the Standard Templates section: Standard Templates section now includes: - Problem/Solution - Meeting - Research - User Story (manual template) - ✨ User Story with AI (AI-powered generation) - Quick Note Changes: - Add 'user-story' to STANDARD_TEMPLATES array - Insert AI-enhanced option after regular User Story template - AI option uses sparkle icon (✨) to distinguish it visually - Both options now appear in the collapsible "Standard Templates" section of the Templates panel User can now access user story creation from three places: 1. Templates panel > Standard Templates 2. Command Palette (Cmd+K Cmd+N) 3. Direct command palette search for "Create User Story with AI" * Fix CI/CD and implement code review improvements CI/CD Fix: - Fix TypeScript compilation error in templatesTreeProvider.ts - Explicitly type items array as TreeItem[] to allow mixing TemplateActionItem and ActionButtonItem - Resolves: error TS2345 "Property 'templateType' is missing in type 'ActionButtonItem'" Code Review Improvements: 1. Cancellation Support: - Add cancellable: true to withProgress in handleCreateUserStoryWithAI - Use token from progress callback instead of creating new CancellationTokenSource - Check token.isCancellationRequested during response streaming - Add early return if cancelled to prevent partial operations 2. ES6 Import Refactoring: - Replace require('os') with top-level import * as os from 'os' - Change require('os').userInfo().username to os.userInfo().username - Improves code readability and enables static analysis 3. DRY Principle - Shared AI Model Service: - Create new src/services/aiModelService.ts with shared selectAIModel() - Remove duplicate selectModel() from TemplateGenerator.ts (84 lines) - Remove duplicate selectAIModel() from templateCommands.ts (75 lines) - Both files now import and use shared selectAIModel() - Single source of truth for AI model selection logic - Prioritizes Claude Sonnet (latest) > Opus > other Claude > GPT-4 > GPT > Gemini Benefits: - Easier to maintain model selection logic in one place - Consistent behavior across all AI features - Reduced code duplication by ~160 lines - Future AI features can reuse the same selection logic --------- Co-authored-by: Claude <[email protected]>
1 parent d0a5d68 commit e40ffed

File tree

7 files changed

+458
-59
lines changed

7 files changed

+458
-59
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@
248248
"title": "Noted: Select AI Model",
249249
"icon": "$(settings-gear)"
250250
},
251+
{
252+
"command": "noted.createUserStoryWithAI",
253+
"title": "Noted: Create User Story with AI",
254+
"icon": "$(notebook)"
255+
},
251256
{
252257
"command": "noted.createBundle",
253258
"title": "Noted: Create Notes from Bundle",

src/commands/templateCommands.ts

Lines changed: 270 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import * as vscode from 'vscode';
22
import * as path from 'path';
3+
import * as os from 'os';
34
import { templateGenerator } from '../templates/TemplateGenerator';
45
import { Template } from '../templates/TemplateTypes';
5-
import { getTemplatesPath, getFileFormat } from '../services/configService';
6+
import { getTemplatesPath, getFileFormat, getNotesPath } from '../services/configService';
67
import { writeFile, readFile, pathExists, createDirectory } from '../services/fileSystemService';
78
import { getCustomTemplates } from '../services/templateService';
89
import { BUILT_IN_PLACEHOLDER_NAMES, TEMPLATE_CATEGORY_KEYWORDS } from '../constants';
10+
import { selectAIModel } from '../services/aiModelService';
911

1012
/**
1113
* Command: Create a new template using AI generation
@@ -740,3 +742,270 @@ export function extractVariablesFromContent(content: string): Template['variable
740742

741743
return variables;
742744
}
745+
746+
/**
747+
* Command: Create a user story with AI-populated content
748+
* Specifically designed for the user-story template
749+
*/
750+
export async function handleCreateUserStoryWithAI(): Promise<void> {
751+
try {
752+
// Prompt user for a brief description of the user story
753+
const description = await vscode.window.showInputBox({
754+
prompt: 'Brief description of the user story',
755+
placeHolder: 'e.g., "Allow users to reset their password via email link"',
756+
validateInput: (value) => {
757+
if (!value || value.trim().length < 5) {
758+
return 'Please provide a brief description (at least 5 characters)';
759+
}
760+
return null;
761+
}
762+
});
763+
764+
if (!description) {
765+
return;
766+
}
767+
768+
// Show progress indicator with cancellation support
769+
await vscode.window.withProgress({
770+
location: vscode.ProgressLocation.Notification,
771+
title: 'Generating user story with AI...',
772+
cancellable: true
773+
}, async (progress, token) => {
774+
// Listen for cancellation
775+
token.onCancellationRequested(() => {
776+
console.log('User canceled the user story generation.');
777+
});
778+
779+
progress.report({ increment: 0, message: 'Analyzing description...' });
780+
781+
// Build a specialized prompt for user story generation
782+
const userStoryPrompt = buildUserStoryPrompt(description);
783+
784+
// Select the appropriate language model
785+
const model = await selectAIModel();
786+
787+
// Create messages for the LLM
788+
const messages = [
789+
vscode.LanguageModelChatMessage.User(userStoryPrompt)
790+
];
791+
792+
// Send request with cancellation support using the token from progress
793+
const response = await model.sendRequest(messages, {}, token);
794+
795+
progress.report({ increment: 50, message: 'Populating user story...' });
796+
797+
// Collect the full response with cancellation checks
798+
let fullResponse = '';
799+
for await (const fragment of response.text) {
800+
if (token.isCancellationRequested) {
801+
return;
802+
}
803+
fullResponse += fragment;
804+
}
805+
806+
if (token.isCancellationRequested) {
807+
return;
808+
}
809+
810+
// Parse the AI response
811+
const userStory = parseUserStoryResponse(fullResponse, description);
812+
813+
progress.report({ increment: 75, message: 'Creating note...' });
814+
815+
// Create the note with the populated content
816+
await createUserStoryNote(userStory);
817+
818+
progress.report({ increment: 100, message: 'Done!' });
819+
820+
vscode.window.showInformationMessage(
821+
`✅ User story "${userStory.title}" created successfully!`
822+
);
823+
});
824+
} catch (error) {
825+
if (error instanceof Error) {
826+
vscode.window.showErrorMessage(`Failed to create user story: ${error.message}`);
827+
} else {
828+
vscode.window.showErrorMessage('Failed to create user story: Unknown error');
829+
}
830+
}
831+
}
832+
833+
/**
834+
* Build a specialized prompt for user story generation
835+
*/
836+
function buildUserStoryPrompt(description: string): string {
837+
return `You are a user story generation assistant for agile software development.
838+
839+
User wants to create a user story for: "${description}"
840+
841+
Generate a complete user story with:
842+
1. A concise story title
843+
2. User story description in the format "As a [user type], I want [goal], So that [benefit]"
844+
3. A list of 3-5 specific, actionable tasks needed to complete this story
845+
4. A list of 3-5 acceptance criteria that define when the story is complete
846+
5. A realistic time estimate (in hours or days)
847+
848+
Output format (JSON):
849+
{
850+
"title": "Brief title for the user story",
851+
"user_type": "type of user (e.g., customer, admin, developer)",
852+
"goal": "what the user wants to achieve",
853+
"benefit": "why this is valuable",
854+
"tasks": [
855+
"Task 1 - specific implementation step",
856+
"Task 2 - specific implementation step",
857+
"Task 3 - specific implementation step"
858+
],
859+
"acceptance_criteria": [
860+
"Criterion 1 - specific, testable outcome",
861+
"Criterion 2 - specific, testable outcome",
862+
"Criterion 3 - specific, testable outcome"
863+
],
864+
"time_estimate": "X hours" or "X days"
865+
}
866+
867+
RULES:
868+
- Make the user_type, goal, and benefit specific to the context
869+
- Tasks should be actionable development steps
870+
- Acceptance criteria should be specific and testable
871+
- Time estimates should be realistic for the complexity described
872+
- All fields are required
873+
874+
Return ONLY valid JSON, no other text.`;
875+
}
876+
877+
/**
878+
* Parse the AI response into a structured user story object
879+
*/
880+
interface UserStory {
881+
title: string;
882+
user_type: string;
883+
goal: string;
884+
benefit: string;
885+
tasks: string[];
886+
acceptance_criteria: string[];
887+
time_estimate: string;
888+
}
889+
890+
function parseUserStoryResponse(response: string, fallbackDescription: string): UserStory {
891+
try {
892+
// Remove markdown code blocks if present
893+
const cleaned = response
894+
.replace(/```json\n?/gi, '')
895+
.replace(/```\n?/g, '')
896+
.trim();
897+
898+
// Parse JSON
899+
const parsed = JSON.parse(cleaned);
900+
901+
// Validate required fields
902+
if (!parsed.title || !parsed.user_type || !parsed.goal || !parsed.benefit) {
903+
throw new Error('Response missing required fields');
904+
}
905+
906+
return {
907+
title: parsed.title,
908+
user_type: parsed.user_type,
909+
goal: parsed.goal,
910+
benefit: parsed.benefit,
911+
tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
912+
acceptance_criteria: Array.isArray(parsed.acceptance_criteria) ? parsed.acceptance_criteria : [],
913+
time_estimate: parsed.time_estimate || 'TBD'
914+
};
915+
} catch (error) {
916+
console.error('Failed to parse user story response:', error);
917+
console.error('Response was:', response);
918+
919+
// Fallback to basic structure
920+
return {
921+
title: fallbackDescription,
922+
user_type: 'user',
923+
goal: fallbackDescription,
924+
benefit: 'improve the system',
925+
tasks: ['Implement the feature', 'Write tests', 'Deploy to production'],
926+
acceptance_criteria: ['Feature works as expected', 'Tests pass', 'No regressions'],
927+
time_estimate: 'TBD'
928+
};
929+
}
930+
}
931+
932+
/**
933+
* Create a note file with the populated user story content
934+
*/
935+
async function createUserStoryNote(userStory: UserStory): Promise<void> {
936+
const notesPath = getNotesPath();
937+
if (!notesPath) {
938+
throw new Error('Notes folder is not configured. Please set up your notes folder first.');
939+
}
940+
941+
const fileFormat = getFileFormat();
942+
943+
// Sanitize title for filename
944+
const sanitizedTitle = userStory.title
945+
.toLowerCase()
946+
.replace(/\s+/g, '-')
947+
.replace(/[^a-z0-9-_]/g, '');
948+
949+
// Create the file path in the "User Stories" folder
950+
const userStoriesFolder = path.join(notesPath, 'User Stories');
951+
await createDirectory(userStoriesFolder);
952+
953+
const fileName = `${sanitizedTitle}.${fileFormat}`;
954+
const filePath = path.join(userStoriesFolder, fileName);
955+
956+
// Build the note content
957+
const date = new Date();
958+
const dateStr = date.toLocaleString('en-US', {
959+
weekday: 'long',
960+
year: 'numeric',
961+
month: 'long',
962+
day: 'numeric'
963+
});
964+
const user = os.userInfo().username;
965+
966+
// Format tasks as checklist
967+
const tasksList = userStory.tasks.map(task => `- [ ] ${task}`).join('\n');
968+
969+
// Format acceptance criteria as checklist
970+
const acceptanceCriteriaList = userStory.acceptance_criteria.map(criterion => `- [ ] ${criterion}`).join('\n');
971+
972+
const content = `---
973+
tags: [user-story]
974+
created: ${date.toISOString()}
975+
status: draft
976+
---
977+
978+
# User Story: ${userStory.title}
979+
980+
**Created:** ${dateStr} | **Author:** ${user}
981+
982+
## Story Title
983+
${userStory.title}
984+
985+
## Description
986+
As a ${userStory.user_type},
987+
I want ${userStory.goal},
988+
So that ${userStory.benefit}.
989+
990+
### Tasks
991+
${tasksList}
992+
993+
## Acceptance Criteria
994+
${acceptanceCriteriaList}
995+
996+
## Estimate
997+
**Time to Complete:** ${userStory.time_estimate}
998+
999+
## Notes
1000+
[Additional context, dependencies, or references]
1001+
1002+
---
1003+
`;
1004+
1005+
// Write the file
1006+
await writeFile(filePath, content);
1007+
1008+
// Open the file
1009+
const doc = await vscode.workspace.openTextDocument(filePath);
1010+
await vscode.window.showTextDocument(doc);
1011+
}

src/constants.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,37 @@ NEXT STEPS:
4747
4848
4949
`,
50-
'quick': () => ``
50+
'quick': () => ``,
51+
'user-story': () => `# User Story: {filename}
52+
53+
**Created:** {date} | **Author:** {user}
54+
55+
## Story Title
56+
[Brief title for the user story]
57+
58+
## Description
59+
As a [user type],
60+
I want [goal],
61+
So that [benefit].
62+
63+
### Tasks
64+
- [ ] Task 1
65+
- [ ] Task 2
66+
- [ ] Task 3
67+
68+
## Acceptance Criteria
69+
- [ ] Criterion 1
70+
- [ ] Criterion 2
71+
- [ ] Criterion 3
72+
73+
## Estimate
74+
**Time to Complete:** [X hours/days]
75+
76+
## Notes
77+
[Additional context, dependencies, or references]
78+
79+
---
80+
`
5181
};
5282

5383
/**
@@ -57,6 +87,7 @@ export const BUILT_IN_TEMPLATE_INFO = [
5787
{ label: 'Problem/Solution', value: 'problem-solution', description: 'Document bugs and troubleshooting' },
5888
{ label: 'Meeting', value: 'meeting', description: 'Organize meeting notes and action items' },
5989
{ label: 'Research', value: 'research', description: 'Structure your research and findings' },
90+
{ label: 'User Story', value: 'user-story', description: 'Agile user stories with tasks, criteria, and estimates' },
6091
{ label: 'Quick', value: 'quick', description: 'Simple dated note' }
6192
];
6293

@@ -131,7 +162,9 @@ export const TEMPLATE_CATEGORY_KEYWORDS: Record<string, string> = {
131162
'project': 'Projects',
132163
'video': 'Content Creation',
133164
'tutorial': 'Content Creation',
134-
'research': 'Research'
165+
'research': 'Research',
166+
'user-story': 'User Stories',
167+
'story': 'User Stories'
135168
};
136169

137170
/**
@@ -206,6 +239,12 @@ export const DEFAULT_CATEGORIES: Record<string, CategoryConfig> = {
206239
templates: ['problem-solution'],
207240
description: 'Project notes and problem-solving'
208241
},
242+
'User Stories': {
243+
folder: 'User Stories',
244+
icon: '📝',
245+
templates: ['user-story'],
246+
description: 'Agile user stories with acceptance criteria'
247+
},
209248
'Quick': {
210249
folder: 'Quick',
211250
icon: '⚡',

0 commit comments

Comments
 (0)