promptarchitect 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscodeignore +7 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +44 -0
- package/README.md +200 -0
- package/docs/CHAT_UI_REDESIGN_PLAN.md +371 -0
- package/images/hub-icon.svg +6 -0
- package/images/prompt-lab-icon.svg +11 -0
- package/package.json +519 -0
- package/src/agentPrompts.ts +278 -0
- package/src/agentService.ts +630 -0
- package/src/api.ts +223 -0
- package/src/authService.ts +556 -0
- package/src/chatPanel.ts +979 -0
- package/src/extension.ts +822 -0
- package/src/providers/aiChatViewProvider.ts +1023 -0
- package/src/providers/environmentTreeProvider.ts +311 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/notesTreeProvider.ts +301 -0
- package/src/providers/quickAccessTreeProvider.ts +328 -0
- package/src/providers/scriptsTreeProvider.ts +324 -0
- package/src/refinerPanel.ts +620 -0
- package/src/templates.ts +61 -0
- package/src/workspaceIndexer.ts +766 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Tree View Provider
|
|
3
|
+
*
|
|
4
|
+
* Displays environment variables from .env files and provides
|
|
5
|
+
* quick copy/edit functionality.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as vscode from 'vscode';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
|
|
12
|
+
export interface EnvVariable {
|
|
13
|
+
key: string;
|
|
14
|
+
value: string;
|
|
15
|
+
source: string; // filename like '.env', '.env.local'
|
|
16
|
+
isSecret: boolean; // Heuristic based on key name
|
|
17
|
+
lineNumber: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class EnvironmentTreeProvider implements vscode.TreeDataProvider<EnvTreeItem> {
|
|
21
|
+
private _onDidChangeTreeData: vscode.EventEmitter<EnvTreeItem | undefined | null | void> =
|
|
22
|
+
new vscode.EventEmitter<EnvTreeItem | undefined | null | void>();
|
|
23
|
+
readonly onDidChangeTreeData: vscode.Event<EnvTreeItem | undefined | null | void> =
|
|
24
|
+
this._onDidChangeTreeData.event;
|
|
25
|
+
|
|
26
|
+
private envFiles: Map<string, EnvVariable[]> = new Map();
|
|
27
|
+
private revealedSecrets: Set<string> = new Set(); // key@source format
|
|
28
|
+
|
|
29
|
+
// Patterns that suggest a value is secret
|
|
30
|
+
private readonly SECRET_PATTERNS = [
|
|
31
|
+
/secret/i,
|
|
32
|
+
/password/i,
|
|
33
|
+
/api[_-]?key/i,
|
|
34
|
+
/token/i,
|
|
35
|
+
/private/i,
|
|
36
|
+
/auth/i,
|
|
37
|
+
/credential/i,
|
|
38
|
+
/key$/i,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Common env file names to look for
|
|
42
|
+
private readonly ENV_FILES = [
|
|
43
|
+
'.env',
|
|
44
|
+
'.env.local',
|
|
45
|
+
'.env.development',
|
|
46
|
+
'.env.production',
|
|
47
|
+
'.env.test',
|
|
48
|
+
'.env.example',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
constructor(private context: vscode.ExtensionContext) {
|
|
52
|
+
this.detectEnvFiles();
|
|
53
|
+
|
|
54
|
+
// Watch for .env file changes
|
|
55
|
+
const watcher = vscode.workspace.createFileSystemWatcher('**/.env*');
|
|
56
|
+
watcher.onDidChange(() => this.refresh());
|
|
57
|
+
watcher.onDidCreate(() => this.refresh());
|
|
58
|
+
watcher.onDidDelete(() => this.refresh());
|
|
59
|
+
context.subscriptions.push(watcher);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
refresh(): void {
|
|
63
|
+
this.detectEnvFiles();
|
|
64
|
+
this._onDidChangeTreeData.fire();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getTreeItem(element: EnvTreeItem): vscode.TreeItem {
|
|
68
|
+
return element;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getChildren(element?: EnvTreeItem): Promise<EnvTreeItem[]> {
|
|
72
|
+
if (!vscode.workspace.workspaceFolders) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.envFiles.size === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!element) {
|
|
81
|
+
// Root level - show env file categories
|
|
82
|
+
const items: EnvTreeItem[] = [];
|
|
83
|
+
|
|
84
|
+
for (const [source, vars] of this.envFiles.entries()) {
|
|
85
|
+
if (vars.length > 0) {
|
|
86
|
+
items.push(new EnvTreeItem(
|
|
87
|
+
source,
|
|
88
|
+
vscode.TreeItemCollapsibleState.Expanded,
|
|
89
|
+
'envFile',
|
|
90
|
+
undefined,
|
|
91
|
+
source
|
|
92
|
+
));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return items;
|
|
97
|
+
} else {
|
|
98
|
+
// Children - show variables for this file
|
|
99
|
+
const source = element.source || '';
|
|
100
|
+
const vars = this.envFiles.get(source) || [];
|
|
101
|
+
|
|
102
|
+
return vars.map(envVar => {
|
|
103
|
+
const isRevealed = this.revealedSecrets.has(`${envVar.key}@${envVar.source}`);
|
|
104
|
+
const displayValue = envVar.isSecret && !isRevealed
|
|
105
|
+
? '••••••••••'
|
|
106
|
+
: envVar.value;
|
|
107
|
+
|
|
108
|
+
const item = new EnvTreeItem(
|
|
109
|
+
envVar.key,
|
|
110
|
+
vscode.TreeItemCollapsibleState.None,
|
|
111
|
+
'envVar',
|
|
112
|
+
envVar,
|
|
113
|
+
envVar.source
|
|
114
|
+
);
|
|
115
|
+
item.description = displayValue;
|
|
116
|
+
item.tooltip = new vscode.MarkdownString(
|
|
117
|
+
`**${envVar.key}**\n\n` +
|
|
118
|
+
`Value: \`${envVar.isSecret && !isRevealed ? '(hidden)' : envVar.value}\`\n\n` +
|
|
119
|
+
`Source: ${envVar.source}\n\n` +
|
|
120
|
+
`Line: ${envVar.lineNumber}\n\n` +
|
|
121
|
+
`_Click to copy value_`
|
|
122
|
+
);
|
|
123
|
+
item.iconPath = new vscode.ThemeIcon(
|
|
124
|
+
envVar.isSecret ? 'key' : 'symbol-variable'
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return item;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private detectEnvFiles(): void {
|
|
133
|
+
this.envFiles.clear();
|
|
134
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
135
|
+
if (!workspaceFolder) return;
|
|
136
|
+
|
|
137
|
+
const rootPath = workspaceFolder.uri.fsPath;
|
|
138
|
+
|
|
139
|
+
for (const envFileName of this.ENV_FILES) {
|
|
140
|
+
const envPath = path.join(rootPath, envFileName);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
if (fs.existsSync(envPath)) {
|
|
144
|
+
const variables = this.parseEnvFile(envPath, envFileName);
|
|
145
|
+
if (variables.length > 0) {
|
|
146
|
+
this.envFiles.set(envFileName, variables);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`Error reading ${envFileName}:`, error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private parseEnvFile(filePath: string, source: string): EnvVariable[] {
|
|
156
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
157
|
+
const lines = content.split('\n');
|
|
158
|
+
const variables: EnvVariable[] = [];
|
|
159
|
+
|
|
160
|
+
lines.forEach((line, index) => {
|
|
161
|
+
// Skip comments and empty lines
|
|
162
|
+
const trimmedLine = line.trim();
|
|
163
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parse KEY=VALUE format
|
|
168
|
+
const match = trimmedLine.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
169
|
+
if (match) {
|
|
170
|
+
const key = match[1];
|
|
171
|
+
let value = match[2];
|
|
172
|
+
|
|
173
|
+
// Remove surrounding quotes if present
|
|
174
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
175
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
176
|
+
value = value.slice(1, -1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if this looks like a secret
|
|
180
|
+
const isSecret = this.SECRET_PATTERNS.some(pattern => pattern.test(key));
|
|
181
|
+
|
|
182
|
+
variables.push({
|
|
183
|
+
key,
|
|
184
|
+
value,
|
|
185
|
+
source,
|
|
186
|
+
isSecret,
|
|
187
|
+
lineNumber: index + 1,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return variables;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async copyValue(item: EnvTreeItem): Promise<void> {
|
|
196
|
+
if (!item.envVar) return;
|
|
197
|
+
|
|
198
|
+
await vscode.env.clipboard.writeText(item.envVar.value);
|
|
199
|
+
vscode.window.showInformationMessage(`📋 Copied ${item.envVar.key} value`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
toggleReveal(item: EnvTreeItem): void {
|
|
203
|
+
if (!item.envVar) return;
|
|
204
|
+
|
|
205
|
+
const key = `${item.envVar.key}@${item.envVar.source}`;
|
|
206
|
+
if (this.revealedSecrets.has(key)) {
|
|
207
|
+
this.revealedSecrets.delete(key);
|
|
208
|
+
} else {
|
|
209
|
+
this.revealedSecrets.add(key);
|
|
210
|
+
}
|
|
211
|
+
this._onDidChangeTreeData.fire();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async createEnvFile(): Promise<void> {
|
|
215
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
216
|
+
if (!workspaceFolder) {
|
|
217
|
+
vscode.window.showWarningMessage('No workspace folder open');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const envPath = path.join(workspaceFolder.uri.fsPath, '.env');
|
|
222
|
+
|
|
223
|
+
if (fs.existsSync(envPath)) {
|
|
224
|
+
// Open existing file
|
|
225
|
+
const doc = await vscode.workspace.openTextDocument(envPath);
|
|
226
|
+
await vscode.window.showTextDocument(doc);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Create new .env file with template
|
|
231
|
+
const template = `# Environment Variables
|
|
232
|
+
# Add your environment-specific variables here
|
|
233
|
+
|
|
234
|
+
# Example:
|
|
235
|
+
# DATABASE_URL=postgres://user:pass@localhost:5432/db
|
|
236
|
+
# API_KEY=your-api-key-here
|
|
237
|
+
# NODE_ENV=development
|
|
238
|
+
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
fs.writeFileSync(envPath, template);
|
|
242
|
+
const doc = await vscode.workspace.openTextDocument(envPath);
|
|
243
|
+
await vscode.window.showTextDocument(doc);
|
|
244
|
+
|
|
245
|
+
this.refresh();
|
|
246
|
+
vscode.window.showInformationMessage('Created .env file');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async openEnvFile(item: EnvTreeItem): Promise<void> {
|
|
250
|
+
const source = item.source;
|
|
251
|
+
if (!source) return;
|
|
252
|
+
|
|
253
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
|
254
|
+
if (!workspaceFolder) return;
|
|
255
|
+
|
|
256
|
+
const envPath = path.join(workspaceFolder.uri.fsPath, source);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const doc = await vscode.workspace.openTextDocument(envPath);
|
|
260
|
+
await vscode.window.showTextDocument(doc);
|
|
261
|
+
|
|
262
|
+
// If we have a specific line number, go to it
|
|
263
|
+
if (item.envVar && item.envVar.lineNumber > 0) {
|
|
264
|
+
const editor = vscode.window.activeTextEditor;
|
|
265
|
+
if (editor) {
|
|
266
|
+
const line = item.envVar.lineNumber - 1;
|
|
267
|
+
const position = new vscode.Position(line, 0);
|
|
268
|
+
editor.selection = new vscode.Selection(position, position);
|
|
269
|
+
editor.revealRange(new vscode.Range(position, position));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
vscode.window.showErrorMessage(`Could not open ${source}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export class EnvTreeItem extends vscode.TreeItem {
|
|
279
|
+
public envVar?: EnvVariable;
|
|
280
|
+
public source?: string;
|
|
281
|
+
|
|
282
|
+
constructor(
|
|
283
|
+
public readonly label: string,
|
|
284
|
+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
|
285
|
+
public readonly contextValue: string,
|
|
286
|
+
envVar?: EnvVariable,
|
|
287
|
+
source?: string
|
|
288
|
+
) {
|
|
289
|
+
super(label, collapsibleState);
|
|
290
|
+
this.envVar = envVar;
|
|
291
|
+
this.source = source;
|
|
292
|
+
|
|
293
|
+
if (contextValue === 'envFile') {
|
|
294
|
+
this.iconPath = new vscode.ThemeIcon('file');
|
|
295
|
+
this.tooltip = `Environment file: ${source}`;
|
|
296
|
+
// Make clickable to open the file
|
|
297
|
+
this.command = {
|
|
298
|
+
command: 'promptarchitect.openEnvFile',
|
|
299
|
+
title: 'Open File',
|
|
300
|
+
arguments: [this],
|
|
301
|
+
};
|
|
302
|
+
} else if (contextValue === 'envVar') {
|
|
303
|
+
// Make clickable to copy
|
|
304
|
+
this.command = {
|
|
305
|
+
command: 'promptarchitect.copyEnvValue',
|
|
306
|
+
title: 'Copy Value',
|
|
307
|
+
arguments: [this],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ScriptsTreeProvider, ScriptTreeItem } from './scriptsTreeProvider';
|
|
6
|
+
export { QuickAccessTreeProvider, QuickAccessTreeItem } from './quickAccessTreeProvider';
|
|
7
|
+
export { EnvironmentTreeProvider, EnvTreeItem } from './environmentTreeProvider';
|
|
8
|
+
export { NotesTreeProvider, NoteTreeItem } from './notesTreeProvider';
|
|
9
|
+
export { AIChatViewProvider } from './aiChatViewProvider';
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notes Tree View Provider
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, project-scoped TODO list and scratchpad.
|
|
5
|
+
* Notes are persisted per-project in workspace state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as vscode from 'vscode';
|
|
9
|
+
|
|
10
|
+
export interface NoteItem {
|
|
11
|
+
id: string;
|
|
12
|
+
text: string;
|
|
13
|
+
completed: boolean;
|
|
14
|
+
priority: 'none' | 'high' | 'medium' | 'low';
|
|
15
|
+
createdAt: number;
|
|
16
|
+
completedAt?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class NotesTreeProvider implements vscode.TreeDataProvider<NoteTreeItem> {
|
|
20
|
+
private _onDidChangeTreeData: vscode.EventEmitter<NoteTreeItem | undefined | null | void> =
|
|
21
|
+
new vscode.EventEmitter<NoteTreeItem | undefined | null | void>();
|
|
22
|
+
readonly onDidChangeTreeData: vscode.Event<NoteTreeItem | undefined | null | void> =
|
|
23
|
+
this._onDidChangeTreeData.event;
|
|
24
|
+
|
|
25
|
+
private notes: NoteItem[] = [];
|
|
26
|
+
|
|
27
|
+
constructor(private context: vscode.ExtensionContext) {
|
|
28
|
+
// Load notes from workspace storage
|
|
29
|
+
this.notes = context.workspaceState.get('projectNotes', []);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
refresh(): void {
|
|
33
|
+
this._onDidChangeTreeData.fire();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getTreeItem(element: NoteTreeItem): vscode.TreeItem {
|
|
37
|
+
return element;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getChildren(element?: NoteTreeItem): Promise<NoteTreeItem[]> {
|
|
41
|
+
if (!element) {
|
|
42
|
+
// Root level - show categories
|
|
43
|
+
const activeNotes = this.notes.filter(n => !n.completed);
|
|
44
|
+
const completedNotes = this.notes.filter(n => n.completed);
|
|
45
|
+
|
|
46
|
+
const items: NoteTreeItem[] = [];
|
|
47
|
+
|
|
48
|
+
// Active notes (shown directly, no category header if few)
|
|
49
|
+
if (activeNotes.length > 0) {
|
|
50
|
+
// Sort by priority then by creation date
|
|
51
|
+
const sorted = this.sortNotes(activeNotes);
|
|
52
|
+
sorted.forEach(note => {
|
|
53
|
+
items.push(this.createNoteTreeItem(note));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Completed section (collapsible)
|
|
58
|
+
if (completedNotes.length > 0) {
|
|
59
|
+
items.push(new NoteTreeItem(
|
|
60
|
+
`Completed (${completedNotes.length})`,
|
|
61
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
62
|
+
'completedCategory',
|
|
63
|
+
undefined
|
|
64
|
+
));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return items;
|
|
68
|
+
} else if (element.contextValue === 'completedCategory') {
|
|
69
|
+
// Show completed notes
|
|
70
|
+
const completedNotes = this.notes.filter(n => n.completed);
|
|
71
|
+
const sorted = completedNotes.sort((a, b) => (b.completedAt || 0) - (a.completedAt || 0));
|
|
72
|
+
return sorted.map(note => this.createNoteTreeItem(note));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private sortNotes(notes: NoteItem[]): NoteItem[] {
|
|
79
|
+
const priorityOrder: Record<string, number> = {
|
|
80
|
+
'high': 0,
|
|
81
|
+
'medium': 1,
|
|
82
|
+
'low': 2,
|
|
83
|
+
'none': 3,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return notes.sort((a, b) => {
|
|
87
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
88
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
89
|
+
return a.createdAt - b.createdAt;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private createNoteTreeItem(note: NoteItem): NoteTreeItem {
|
|
94
|
+
const item = new NoteTreeItem(
|
|
95
|
+
note.text,
|
|
96
|
+
vscode.TreeItemCollapsibleState.None,
|
|
97
|
+
note.completed ? 'noteCompleted' : 'noteActive',
|
|
98
|
+
note
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Set checkbox icon
|
|
102
|
+
item.iconPath = new vscode.ThemeIcon(
|
|
103
|
+
note.completed ? 'pass-filled' : 'circle-large-outline'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Add priority indicator
|
|
107
|
+
const priorityIcons: Record<string, string> = {
|
|
108
|
+
'high': '🔴',
|
|
109
|
+
'medium': '🟡',
|
|
110
|
+
'low': '🟢',
|
|
111
|
+
'none': '',
|
|
112
|
+
};
|
|
113
|
+
const priorityIndicator = priorityIcons[note.priority];
|
|
114
|
+
|
|
115
|
+
if (note.completed) {
|
|
116
|
+
item.description = this.getRelativeTime(note.completedAt || note.createdAt);
|
|
117
|
+
} else if (priorityIndicator) {
|
|
118
|
+
item.description = priorityIndicator;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Tooltip with details
|
|
122
|
+
item.tooltip = new vscode.MarkdownString(
|
|
123
|
+
`${note.completed ? '✅' : '☐'} ${note.text}\n\n` +
|
|
124
|
+
`Priority: ${note.priority}\n\n` +
|
|
125
|
+
`Created: ${new Date(note.createdAt).toLocaleString()}\n\n` +
|
|
126
|
+
(note.completed && note.completedAt
|
|
127
|
+
? `Completed: ${new Date(note.completedAt).toLocaleString()}`
|
|
128
|
+
: '_Click to toggle completion_')
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Click to toggle
|
|
132
|
+
item.command = {
|
|
133
|
+
command: 'promptarchitect.toggleNote',
|
|
134
|
+
title: 'Toggle Note',
|
|
135
|
+
arguments: [item],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return item;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private getRelativeTime(timestamp: number): string {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const diff = now - timestamp;
|
|
144
|
+
const minutes = Math.floor(diff / 60000);
|
|
145
|
+
const hours = Math.floor(minutes / 60);
|
|
146
|
+
const days = Math.floor(hours / 24);
|
|
147
|
+
|
|
148
|
+
if (days > 0) return `${days}d ago`;
|
|
149
|
+
if (hours > 0) return `${hours}h ago`;
|
|
150
|
+
if (minutes > 0) return `${minutes}m ago`;
|
|
151
|
+
return 'just now';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private generateId(): string {
|
|
155
|
+
return `note_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async saveNotes(): Promise<void> {
|
|
159
|
+
await this.context.workspaceState.update('projectNotes', this.notes);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async addNote(): Promise<void> {
|
|
163
|
+
const text = await vscode.window.showInputBox({
|
|
164
|
+
prompt: 'Enter your note or TODO',
|
|
165
|
+
placeHolder: 'e.g., Fix authentication bug',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!text) return;
|
|
169
|
+
|
|
170
|
+
// Ask for priority
|
|
171
|
+
const priority = await vscode.window.showQuickPick(
|
|
172
|
+
[
|
|
173
|
+
{ label: '🔴 High', value: 'high' },
|
|
174
|
+
{ label: '🟡 Medium', value: 'medium' },
|
|
175
|
+
{ label: '🟢 Low', value: 'low' },
|
|
176
|
+
{ label: 'No Priority', value: 'none' },
|
|
177
|
+
],
|
|
178
|
+
{
|
|
179
|
+
placeHolder: 'Set priority (optional)',
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const note: NoteItem = {
|
|
184
|
+
id: this.generateId(),
|
|
185
|
+
text,
|
|
186
|
+
completed: false,
|
|
187
|
+
priority: (priority?.value as NoteItem['priority']) || 'none',
|
|
188
|
+
createdAt: Date.now(),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
this.notes.unshift(note);
|
|
192
|
+
await this.saveNotes();
|
|
193
|
+
this._onDidChangeTreeData.fire();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async toggleNote(item: NoteTreeItem): Promise<void> {
|
|
197
|
+
if (!item.note) return;
|
|
198
|
+
|
|
199
|
+
const noteIndex = this.notes.findIndex(n => n.id === item.note!.id);
|
|
200
|
+
if (noteIndex === -1) return;
|
|
201
|
+
|
|
202
|
+
this.notes[noteIndex].completed = !this.notes[noteIndex].completed;
|
|
203
|
+
|
|
204
|
+
if (this.notes[noteIndex].completed) {
|
|
205
|
+
this.notes[noteIndex].completedAt = Date.now();
|
|
206
|
+
} else {
|
|
207
|
+
this.notes[noteIndex].completedAt = undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await this.saveNotes();
|
|
211
|
+
this._onDidChangeTreeData.fire();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async deleteNote(item: NoteTreeItem): Promise<void> {
|
|
215
|
+
if (!item.note) return;
|
|
216
|
+
|
|
217
|
+
const confirm = await vscode.window.showQuickPick(['Yes', 'No'], {
|
|
218
|
+
placeHolder: `Delete "${item.note.text.substring(0, 30)}..."?`,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (confirm !== 'Yes') return;
|
|
222
|
+
|
|
223
|
+
this.notes = this.notes.filter(n => n.id !== item.note!.id);
|
|
224
|
+
await this.saveNotes();
|
|
225
|
+
this._onDidChangeTreeData.fire();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async editNote(item: NoteTreeItem): Promise<void> {
|
|
229
|
+
if (!item.note) return;
|
|
230
|
+
|
|
231
|
+
const newText = await vscode.window.showInputBox({
|
|
232
|
+
prompt: 'Edit note',
|
|
233
|
+
value: item.note.text,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (!newText) return;
|
|
237
|
+
|
|
238
|
+
const noteIndex = this.notes.findIndex(n => n.id === item.note!.id);
|
|
239
|
+
if (noteIndex === -1) return;
|
|
240
|
+
|
|
241
|
+
this.notes[noteIndex].text = newText;
|
|
242
|
+
await this.saveNotes();
|
|
243
|
+
this._onDidChangeTreeData.fire();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async cyclePriority(item: NoteTreeItem): Promise<void> {
|
|
247
|
+
if (!item.note) return;
|
|
248
|
+
|
|
249
|
+
const noteIndex = this.notes.findIndex(n => n.id === item.note!.id);
|
|
250
|
+
if (noteIndex === -1) return;
|
|
251
|
+
|
|
252
|
+
const priorities: NoteItem['priority'][] = ['none', 'high', 'medium', 'low'];
|
|
253
|
+
const currentIndex = priorities.indexOf(this.notes[noteIndex].priority);
|
|
254
|
+
const nextIndex = (currentIndex + 1) % priorities.length;
|
|
255
|
+
|
|
256
|
+
this.notes[noteIndex].priority = priorities[nextIndex];
|
|
257
|
+
await this.saveNotes();
|
|
258
|
+
this._onDidChangeTreeData.fire();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async clearCompleted(): Promise<void> {
|
|
262
|
+
const completedCount = this.notes.filter(n => n.completed).length;
|
|
263
|
+
if (completedCount === 0) {
|
|
264
|
+
vscode.window.showInformationMessage('No completed notes to clear');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const confirm = await vscode.window.showQuickPick(['Yes', 'No'], {
|
|
269
|
+
placeHolder: `Clear ${completedCount} completed note(s)?`,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (confirm !== 'Yes') return;
|
|
273
|
+
|
|
274
|
+
this.notes = this.notes.filter(n => !n.completed);
|
|
275
|
+
await this.saveNotes();
|
|
276
|
+
this._onDidChangeTreeData.fire();
|
|
277
|
+
vscode.window.showInformationMessage(`Cleared ${completedCount} completed note(s)`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
getActiveCount(): number {
|
|
281
|
+
return this.notes.filter(n => !n.completed).length;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export class NoteTreeItem extends vscode.TreeItem {
|
|
286
|
+
public note?: NoteItem;
|
|
287
|
+
|
|
288
|
+
constructor(
|
|
289
|
+
public readonly label: string,
|
|
290
|
+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
|
291
|
+
public readonly contextValue: string,
|
|
292
|
+
note?: NoteItem
|
|
293
|
+
) {
|
|
294
|
+
super(label, collapsibleState);
|
|
295
|
+
this.note = note;
|
|
296
|
+
|
|
297
|
+
if (contextValue === 'completedCategory') {
|
|
298
|
+
this.iconPath = new vscode.ThemeIcon('checklist');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|