Add FOAM_CURRENT_DIR template variable (#1507)

* Added FOAM_CURRENT_DIR template variable

* Added /research-issue Claude command

* Added integration test to create note using FOAM_CURRENT_DIR

* Updated documentation

* fixed comment

* Fail FOAM_CURRENT_DIR resolution if no editor nor workspace is open
This commit is contained in:
Riccardo
2025-09-06 15:25:06 +02:00
committed by GitHub
parent f57b8ec9b6
commit 5cbc722929
9 changed files with 216 additions and 9 deletions

View File

@@ -0,0 +1,61 @@
# Research Issue Command
Research a GitHub issue by analyzing the issue details and codebase to generate a comprehensive task analysis file.
## Usage
```
/research-issue <issue-number>
```
## Parameters
- `issue-number` (required): The GitHub issue number to research
## Description
This command performs comprehensive research on a GitHub issue by:
1. **Fetching Issue Details**: Uses `gh issue view` to get issue title, description, labels, comments, and related information
2. **Codebase Analysis**: Searches the codebase for relevant files, patterns, and components mentioned in the issue
3. **Root Cause Analysis**: Identifies possible technical causes based on the issue description and codebase findings
4. **Solution Planning**: Proposes two solution approaches ranked by preference
5. **Documentation**: Creates a structured task file in `.agent/tasks/<issue-id>-<sanitized-title>.md`
If there is already a `.agent/tasks/<issue-id>-<sanitized-title>.md` file, use it for context and update it accordingly.
If at any time during these steps you need clarifying information from me, please ask.
## Output Format
Creates a markdown file with:
- Issue summary and key details
- Research findings from codebase analysis
- Identified possible root causes
- Two ranked solution approaches with pros/cons
- Technical considerations and dependencies
## Examples
```
/research-issue 1234
/research-issue 567
```
## Implementation
The command will:
1. Validate the issue number and check if it exists
2. Fetch issue details using GitHub CLI
3. Search codebase for relevant patterns, files, and components
4. Analyze findings to identify root causes
5. Generate structured markdown file with research results
6. Save to `.agent/tasks/` directory with standardized naming
## Error Handling
- Invalid issue numbers
- GitHub CLI authentication issues
- Network connectivity problems
- File system write permissions

View File

@@ -119,6 +119,8 @@ This allows features to:
## Development Workflow
We build production code together. I handle implementation details while you guide architecture and catch complexity early.
When working on an issue, check if a `.agent/tasks/<issue-id>-<sanitized-title>.md` exists. If not, suggest whether we should start by doing a research on it (using the `/research-issue <issue-id>`) command.
Whenever we work together on a task, feel free to challenge my assumptions and ideas and be critical if useful.
## Core Workflow: Research → Plan → Implement → Validate

View File

@@ -246,6 +246,7 @@ In addition, you can also use variables provided by Foam:
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
| `FOAM_TITLE_SAFE` | The title of the note in a file system safe format. If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
| `FOAM_SLUG` | The sluggified title of the note (using the default github slug method). If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
| `FOAM_CURRENT_DIR` | The current editor's directory path. Resolves to the directory of the currently active file, or falls back to workspace root if no editor is active. Useful for creating notes in the current directory context. |
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, `FOAM_DATE_WEEK` etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
### `FOAM_DATE_*` variables
@@ -306,6 +307,30 @@ foam_template:
# $FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE Daily Notes
```
##### Creating notes in the current directory
To create notes in the same directory as your currently active file, use the `FOAM_CURRENT_DIR` variable in your template's `filepath`:
```markdown
---
foam_template:
name: Current Directory Note
filepath: '$FOAM_CURRENT_DIR/$FOAM_SLUG.md'
---
# $FOAM_TITLE
$FOAM_SELECTED_TEXT
```
**Best practices for filepath patterns:**
- **Explicit current directory:** `$FOAM_CURRENT_DIR/$FOAM_SLUG.md` - Creates notes in the current editor's directory
- **Workspace root:** `/$FOAM_SLUG.md` - Always creates notes in workspace root
- **Subdirectories:** `$FOAM_CURRENT_DIR/meetings/$FOAM_SLUG.md` - Creates notes in subdirectories relative to current location
The `FOAM_CURRENT_DIR` approach is recommended over relative paths (like `./file.md`) because it makes the template's behavior explicit and doesn't depend on configuration settings.
#### `name` and `description` attributes
These attributes provide a human readable name and description to be shown in the template picker (e.g. When a user uses the `Foam: Create New Note From Template` command):

View File

@@ -309,4 +309,64 @@ foam_template:
expect(doc.getText()).toEqual(`this is my [[hello-world]]`);
});
});
describe('Template filepath with FOAM_CURRENT_DIR', () => {
it('should create note in current directory using FOAM_CURRENT_DIR variable', async () => {
// Create a test subdirectory and a file in it
const noteInSubdir = await createFile('Test content', [
'subdir',
'existing-note.md',
]);
// Create a template with FOAM_CURRENT_DIR variable
const template = await createFile(
`---
foam_template:
filepath: \${FOAM_CURRENT_DIR}/\${FOAM_SLUG}.md
---
# \${FOAM_TITLE}
Template content using FOAM_CURRENT_DIR`,
['.foam', 'templates', 'foam-current-dir-template.md']
);
// Switch to the file in the subdirectory to set current editor context
await showInEditor(noteInSubdir.uri);
// Create a note using the template - FOAM_CURRENT_DIR should resolve to current editor directory
const resultInSubdir = await createNote(
{
templatePath: template.uri.path,
title: 'My New Note',
},
{} as any
);
// The note should be created in the subdir because FOAM_CURRENT_DIR resolves to current editor directory
expect(resultInSubdir.uri).toEqual(
noteInSubdir.uri.getDirectory().joinPath('my-new-note.md')
);
await closeEditors();
// Create a note using the template - FOAM_CURRENT_DIR should resolve to current editor directory
const resultInRoot = await createNote(
{
templatePath: template.uri.path,
title: 'My New Note',
},
{} as any
);
// The note should be created in the workspace root because FOAM_CURRENT_DIR resolves to workspace root when no editor is active
expect(resultInRoot.uri).toEqual(
fromVsCodeUri(workspace.workspaceFolders[0].uri).joinPath(
'my-new-note.md'
)
);
// Clean up
await deleteFile(template.uri);
await deleteFile(noteInSubdir.uri);
await deleteFile(resultInRoot.uri);
await deleteFile(resultInSubdir.uri);
});
});
});

View File

@@ -11,7 +11,6 @@ import { TemplateLoader } from '../../services/template-loader';
import { Template } from '../../services/note-creation-types';
import { Resolver } from '../../services/variable-resolver';
import { asAbsoluteWorkspaceUri, fileExists } from '../../services/editor';
import { isSome } from '../../core/utils';
import { CommandDescriptor } from '../../utils/commands';
import { Foam } from '../../core/model/foam';
import { Location } from '../../core/model/location';

View File

@@ -10,8 +10,7 @@ import {
isPlaceholderTrigger,
} from './note-creation-types';
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
import { asAbsoluteUri, URI } from '../core/model/uri';
import { isAbsolute } from 'path';
import { URI } from '../core/model/uri';
/**
* Unified engine for creating notes from both Markdown and JavaScript templates
@@ -57,9 +56,6 @@ export class NoteCreationEngine {
template: Template & { type: 'javascript' },
resolver: Resolver
): Promise<NoteCreationResult> {
// Convert resolver's variables back to extraParams for backward compatibility
const extraParams = resolver.getVariables();
const templateContext: TemplateContext = {
trigger,
resolver,

View File

@@ -240,6 +240,51 @@ describe('variable-resolver, variable resolution', () => {
);
});
});
describe('FOAM_CURRENT_DIR', () => {
it('should resolve to workspace root when no active editor', async () => {
const resolver = new Resolver(new Map<string, string>(), new Date());
const result = await resolver.resolve(new Variable('FOAM_CURRENT_DIR'));
// Should resolve to some directory path
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
});
it('should resolve to current directory when editor is active', async () => {
// Create a test file in a subdirectory
const testFile = await createFile('Test content', [
'test-dir',
'test-file.md',
]);
try {
// Open the file to make it the active editor
await showInEditor(testFile.uri);
const resolver = new Resolver(new Map<string, string>(), new Date());
const result = await resolver.resolve(new Variable('FOAM_CURRENT_DIR'));
// Should resolve to the test-dir directory
expect(typeof result).toBe('string');
expect(result).toContain('test-dir');
} finally {
// Clean up
await deleteFile(testFile.uri);
}
});
it('should be included in known foam variables', async () => {
const input = '${FOAM_CURRENT_DIR}';
const resolver = new Resolver(new Map(), new Date());
const result = await resolver.resolveText(input);
// Should resolve to a directory path, not remain as ${FOAM_CURRENT_DIR}
expect(result).not.toEqual(input);
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
});
});
});
describe('variable-resolver, resolveText', () => {

View File

@@ -1,5 +1,6 @@
import { findSelectionContent } from './editor';
import { window } from 'vscode';
import { findSelectionContent, getCurrentEditorDirectory } from './editor';
import { window, workspace } from 'vscode';
import { fromVsCodeUri } from '../utils/vsc-utils';
import { UserCancelledOperation } from './errors';
import { toSlug } from '../utils/slug';
import {
@@ -13,6 +14,7 @@ const knownFoamVariables = new Set([
'FOAM_TITLE_SAFE',
'FOAM_SLUG',
'FOAM_SELECTED_TEXT',
'FOAM_CURRENT_DIR',
'FOAM_DATE_YEAR',
'FOAM_DATE_YEAR_SHORT',
'FOAM_DATE_MONTH',
@@ -157,6 +159,9 @@ export class Resolver implements VariableResolver {
case 'FOAM_SELECTED_TEXT':
value = Promise.resolve(resolveFoamSelectedText());
break;
case 'FOAM_CURRENT_DIR':
value = Promise.resolve(resolveFoamCurrentDir());
break;
case 'FOAM_DATE_YEAR':
value = Promise.resolve(String(this.foamDate.getFullYear()));
break;
@@ -262,6 +267,21 @@ function resolveFoamSelectedText() {
return findSelectionContent()?.content ?? '';
}
function resolveFoamCurrentDir() {
try {
// Try to get the directory of the currently active editor
const currentDir = getCurrentEditorDirectory();
return currentDir.toFsPath();
} catch (error) {
// Fall back to workspace root if no active editor
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
return fromVsCodeUri(workspace.workspaceFolders[0].uri).toFsPath();
}
// If no workspace is open, raise
throw new Error('No workspace is open');
}
}
/**
* Common chars that is better to avoid in file names.
* Inspired by:

View File

@@ -9,7 +9,6 @@ import { Logger } from '../core/utils/log';
import { URI } from '../core/model/uri';
import { Resource } from '../core/model/note';
import { randomString, wait } from './test-utils';
import { FoamWorkspace } from '../core/model/workspace';
import { Foam } from '../core/model/foam';
Logger.setLevel('error');