Skip to content

Commit 7eedf4e

Browse files
authored
Add create action items context menu feature (#131)
* Add action items extraction context menu feature - Add ActionItemsService for AI-powered action item extraction using VS Code LLM API - Add ActionItemsCommands with two format options: checkboxes and bullets - Create submenu "Create Action Items" in Noted editor context menu - Insert extracted action items below selection with "## Action Items" header - Show "No action items identified" when AI finds no actionable content * Fix action items extraction to not insert when no items found - Add NO_ACTION_ITEMS_MESSAGE constant for consistent string comparison - Add MAX_INPUT_CHARACTERS constant to replace magic number - Return early without inserting when no action items identified - Use constants in prompt and truncation logic
1 parent 27459ba commit 7eedf4e

File tree

4 files changed

+263
-3
lines changed

4 files changed

+263
-3
lines changed

package.json

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,16 @@
686686
"title": "Summarize Selection",
687687
"icon": "$(sparkle)"
688688
},
689+
{
690+
"command": "noted.extractActionItemsCheckboxes",
691+
"title": "As Checkboxes",
692+
"icon": "$(checklist)"
693+
},
694+
{
695+
"command": "noted.extractActionItemsBullets",
696+
"title": "As Bullet Points",
697+
"icon": "$(list-unordered)"
698+
},
689699
{
690700
"command": "noted.batchGenerateTags",
691701
"title": "Noted: Batch Generate Tags",
@@ -802,6 +812,10 @@
802812
{
803813
"id": "noted.editorContext",
804814
"label": "Noted"
815+
},
816+
{
817+
"id": "noted.actionItemsMenu",
818+
"label": "Create Action Items"
805819
}
806820
],
807821
"menus": {
@@ -816,15 +830,30 @@
816830
"when": "editorHasSelection",
817831
"group": "2_ai@1"
818832
},
833+
{
834+
"submenu": "noted.actionItemsMenu",
835+
"when": "editorHasSelection",
836+
"group": "2_ai@2"
837+
},
819838
{
820839
"command": "noted.summarizeCurrentNote",
821840
"when": "resourcePath =~ /Notes.*\\.(txt|md)$/",
822-
"group": "2_ai@2"
841+
"group": "2_ai@3"
823842
},
824843
{
825844
"command": "noted.generateTagsCurrentNote",
826845
"when": "resourcePath =~ /Notes.*\\.(txt|md)$/",
827-
"group": "2_ai@3"
846+
"group": "2_ai@4"
847+
}
848+
],
849+
"noted.actionItemsMenu": [
850+
{
851+
"command": "noted.extractActionItemsCheckboxes",
852+
"group": "1_format@1"
853+
},
854+
{
855+
"command": "noted.extractActionItemsBullets",
856+
"group": "1_format@2"
828857
}
829858
],
830859
"editor/context": [
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Action Items extraction command handlers
3+
* All commands are designed to be registered in extension.ts
4+
*/
5+
6+
import * as vscode from 'vscode';
7+
import { ActionItemsService, ActionItemFormat } from '../services/actionItemsService';
8+
9+
/**
10+
* Class-based command handlers for Action Items extraction
11+
*/
12+
export class ActionItemsCommands {
13+
constructor(
14+
private actionItemsService: ActionItemsService
15+
) {}
16+
17+
/**
18+
* Handle extracting action items from selection with checkboxes format
19+
*/
20+
public async handleExtractActionItemsCheckboxes(): Promise<void> {
21+
await this.extractActionItemsFromSelection('checkboxes');
22+
}
23+
24+
/**
25+
* Handle extracting action items from selection with bullets format
26+
*/
27+
public async handleExtractActionItemsBullets(): Promise<void> {
28+
await this.extractActionItemsFromSelection('bullets');
29+
}
30+
31+
/**
32+
* Common implementation for extracting action items from selection
33+
*/
34+
private async extractActionItemsFromSelection(format: ActionItemFormat): Promise<void> {
35+
const editor = vscode.window.activeTextEditor;
36+
if (!editor) {
37+
vscode.window.showErrorMessage('No editor is currently open');
38+
return;
39+
}
40+
41+
const selection = editor.selection;
42+
if (selection.isEmpty) {
43+
vscode.window.showErrorMessage('No text is selected. Please select some text to extract action items from.');
44+
return;
45+
}
46+
47+
const selectedText = editor.document.getText(selection);
48+
if (!selectedText.trim()) {
49+
vscode.window.showErrorMessage('Selected text is empty or contains only whitespace');
50+
return;
51+
}
52+
53+
try {
54+
await vscode.window.withProgress(
55+
{
56+
location: vscode.ProgressLocation.Notification,
57+
title: 'Extracting action items...',
58+
cancellable: true
59+
},
60+
async (progress, token) => {
61+
if (token.isCancellationRequested) {
62+
return;
63+
}
64+
65+
const actionItems = await this.actionItemsService.extractActionItems(
66+
selectedText,
67+
format,
68+
token
69+
);
70+
71+
if (token.isCancellationRequested) {
72+
return;
73+
}
74+
75+
// Check if no action items were found
76+
if (actionItems === ActionItemsService.NO_ACTION_ITEMS_MESSAGE) {
77+
vscode.window.showInformationMessage('No action items were identified in the selected text');
78+
return;
79+
}
80+
81+
// Insert action items at the end of the line containing the selection's end point
82+
const endLine = editor.document.lineAt(selection.end.line);
83+
const insertPosition = endLine.range.end;
84+
85+
// Format the action items with header (extra blank line before header)
86+
const actionItemsText = `\n\n## Action Items\n\n${actionItems}\n`;
87+
88+
await editor.edit(editBuilder => {
89+
editBuilder.insert(insertPosition, actionItemsText);
90+
});
91+
92+
vscode.window.showInformationMessage('Action items inserted below selection');
93+
}
94+
);
95+
} catch (error) {
96+
vscode.window.showErrorMessage(`Failed to extract action items: ${error instanceof Error ? error.message : String(error)}`);
97+
}
98+
}
99+
}

src/extension.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ import {
7575
handleClearUndoHistory
7676
} from './commands/undoCommands';
7777
import { SummarizationCommands } from './commands/summarizationCommands';
78+
import { ActionItemsService } from './services/actionItemsService';
79+
import { ActionItemsCommands } from './commands/actionItemsCommands';
7880
import {
7981
handleSuggestTags
8082
} from './commands/autoTagCommands';
@@ -539,6 +541,10 @@ export function activate(context: vscode.ExtensionContext) {
539541
// Initialize summarization command handlers
540542
const summarizationCommands = new SummarizationCommands(summarizationService, context, notesProvider);
541543

544+
// Initialize action items service and commands
545+
const actionItemsService = new ActionItemsService();
546+
const actionItemsCommands = new ActionItemsCommands(actionItemsService);
547+
542548
// Initialize auto-tagging service (Phase 4)
543549
const autoTagService = new AutoTagService();
544550

@@ -2085,6 +2091,14 @@ export function activate(context: vscode.ExtensionContext) {
20852091
await summarizationCommands.handleSummarizeSelection();
20862092
});
20872093

2094+
let extractActionItemsCheckboxes = vscode.commands.registerCommand('noted.extractActionItemsCheckboxes', async () => {
2095+
await actionItemsCommands.handleExtractActionItemsCheckboxes();
2096+
});
2097+
2098+
let extractActionItemsBullets = vscode.commands.registerCommand('noted.extractActionItemsBullets', async () => {
2099+
await actionItemsCommands.handleExtractActionItemsBullets();
2100+
});
2101+
20882102
let summarizeRecent = vscode.commands.registerCommand('noted.summarizeRecent', async () => {
20892103
await summarizationCommands.handleSummarizeRecent();
20902104
});
@@ -2230,7 +2244,7 @@ export function activate(context: vscode.ExtensionContext) {
22302244
showPreview, showMarkdownToolbar,
22312245
undoCommand, redoCommand, showUndoHistory, clearUndoHistory,
22322246
renameSymbol,
2233-
summarizeNote, summarizeCurrentNote, summarizeSelection, summarizeRecent, summarizeWeek, summarizeMonth, summarizeCustomRange, clearSummaryCache, summarizeSearchResults,
2247+
summarizeNote, summarizeCurrentNote, summarizeSelection, extractActionItemsCheckboxes, extractActionItemsBullets, summarizeRecent, summarizeWeek, summarizeMonth, summarizeCustomRange, clearSummaryCache, summarizeSearchResults,
22342248
showSummaryHistory, compareSummaries, restoreSummaryVersion, clearSummaryHistory, showSummaryHistoryStats,
22352249
createPromptTemplate, editPromptTemplate, deletePromptTemplate, duplicatePromptTemplate, listPromptTemplates, viewTemplateVariables,
22362250
suggestTags,

src/services/actionItemsService.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* AI-powered action items extraction service using VS Code Language Model API
3+
* Extracts actionable items from selected text using GitHub Copilot
4+
*/
5+
6+
import * as vscode from 'vscode';
7+
import { selectAIModel } from './aiModelService';
8+
9+
/**
10+
* Format options for action items
11+
*/
12+
export type ActionItemFormat = 'checkboxes' | 'bullets';
13+
14+
/**
15+
* Service for extracting action items from text using AI
16+
*/
17+
export class ActionItemsService {
18+
/** Message returned when no action items are found */
19+
public static readonly NO_ACTION_ITEMS_MESSAGE = 'No action items identified';
20+
21+
/** Maximum characters to send to the AI model */
22+
private static readonly MAX_INPUT_CHARACTERS = 16000;
23+
24+
/**
25+
* Check if Language Model API is available
26+
*/
27+
private async isLanguageModelAvailable(): Promise<boolean> {
28+
try {
29+
await selectAIModel();
30+
return true;
31+
} catch (error) {
32+
return false;
33+
}
34+
}
35+
36+
/**
37+
* Check if AI features are enabled
38+
*/
39+
private isAIEnabled(): boolean {
40+
const config = vscode.workspace.getConfiguration('noted.ai');
41+
return config.get<boolean>('enabled', true);
42+
}
43+
44+
/**
45+
* Build prompt for action item extraction
46+
*/
47+
private buildActionItemsPrompt(selectedText: string, format: ActionItemFormat): string {
48+
const formatInstruction = format === 'checkboxes'
49+
? 'Use checkbox format: - [ ] Action item description'
50+
: 'Use bullet point format: - Action item description';
51+
52+
return `Analyze the following text and extract any actionable items, tasks, or to-dos.
53+
54+
Text to analyze:
55+
---
56+
${selectedText}
57+
---
58+
59+
Instructions:
60+
- Extract clear, specific action items from the text
61+
- ${formatInstruction}
62+
- Include any mentioned deadlines, assignees, or priorities in parentheses
63+
- Group related items together if applicable
64+
- If no clear action items can be identified, respond with exactly: "${ActionItemsService.NO_ACTION_ITEMS_MESSAGE}"
65+
- Do not include any preamble or explanation, only the action items list
66+
67+
Action items:`;
68+
}
69+
70+
/**
71+
* Extract action items from selected text
72+
* @param selectedText The text to analyze for action items
73+
* @param format The format for action items (checkboxes or bullets)
74+
* @param token Cancellation token
75+
* @returns Extracted action items or "No action items identified"
76+
*/
77+
async extractActionItems(
78+
selectedText: string,
79+
format: ActionItemFormat,
80+
token: vscode.CancellationToken
81+
): Promise<string> {
82+
// Check if AI is enabled
83+
if (!this.isAIEnabled()) {
84+
throw new Error('AI features are disabled. Enable them in settings: noted.ai.enabled');
85+
}
86+
87+
// Check if Language Model API is available
88+
if (!(await this.isLanguageModelAvailable())) {
89+
throw new Error('GitHub Copilot is not available. Please install and enable GitHub Copilot to use AI features.');
90+
}
91+
92+
// Truncate if too large
93+
const truncatedText = selectedText.length > ActionItemsService.MAX_INPUT_CHARACTERS
94+
? selectedText.substring(0, ActionItemsService.MAX_INPUT_CHARACTERS) + '\n\n[Text truncated for analysis]'
95+
: selectedText;
96+
97+
// Build prompt
98+
const prompt = this.buildActionItemsPrompt(truncatedText, format);
99+
100+
// Call Language Model API
101+
try {
102+
const model = await selectAIModel();
103+
const messages = [vscode.LanguageModelChatMessage.User(prompt)];
104+
105+
const response = await model.sendRequest(messages, {}, token);
106+
107+
let result = '';
108+
for await (const chunk of response.text) {
109+
result += chunk;
110+
}
111+
112+
return result.trim();
113+
114+
} catch (error) {
115+
throw new Error(`Failed to extract action items: ${error instanceof Error ? error.message : String(error)}`);
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)