Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/file-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,19 @@ export class FileWriter {
return cleaned.length > 0 ? cleaned : "Project";
}

private formatDescription(
originalItem: string,
sourceNoteLink?: string,
description?: string
): string {
// If a description is explicitly provided, use it directly (for manually created projects)
if (description && description.length > 0) {
return description;
}
// Otherwise, format as an inbox item reference
return this.formatOriginalInboxItem(originalItem, sourceNoteLink);
}

private formatOriginalInboxItem(originalItem: string, sourceNoteLink?: string): string {
const normalized = originalItem.replace(/\s+/g, " ").trim();
const sourceSuffix = sourceNoteLink ? ` (${sourceNoteLink})` : "";
Expand Down Expand Up @@ -387,7 +400,7 @@ export class FileWriter {
.replace(/{{\s*sphere\s*}}/g, sphereTagsForTemplate)
.replace(
/{{\s*description\s*}}/g,
this.formatOriginalInboxItem(originalItem, sourceNoteLink)
this.formatDescription(originalItem, sourceNoteLink, result.description)
);

// Process Templater date syntax if present, since we're not using Templater's create_new function
Expand Down Expand Up @@ -489,7 +502,7 @@ export class FileWriter {
): string {
const date = this.formatDate(new Date());
const title = result.projectOutcome || originalItem;
const originalItemDescription = this.formatOriginalInboxItem(originalItem, sourceNoteLink);
const description = this.formatDescription(originalItem, sourceNoteLink, result.description);

// Format sphere tags for YAML list format
const sphereTagsList =
Expand Down Expand Up @@ -522,7 +535,7 @@ status: ${this.settings.defaultStatus}`;

# Description

${originalItemDescription}
${description}

## Next actions
`;
Expand Down
3 changes: 2 additions & 1 deletion src/new-project-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,8 @@ export class NewProjectModal extends Modal {
projectOutcome: this.data.title.trim(),
projectPriority: this.data.priority,
nextAction: this.data.nextAction.trim(),
reasoning: this.data.description.trim() || this.data.title.trim(),
reasoning: "User created project directly",
description: this.data.description.trim(),
recommendedAction: "create-project",
recommendedActionReasoning: "User created project directly",
};
Expand Down
1 change: 1 addition & 0 deletions src/types/gtd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface GTDProcessingResult {
nextAction?: string; // Optional for non-actionable items
nextActions?: string[]; // Support multiple next actions
reasoning: string;
description?: string; // User-provided description for manually created projects
suggestedProjects?: ProjectSuggestion[];
suggestedPersons?: PersonSuggestion[];
recommendedAction: ProcessingAction;
Expand Down
78 changes: 78 additions & 0 deletions tests/file-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,84 @@ status: live
expect(content).toContain("- [ ] Draft proposal outline 📅 2025-11-05");
expect(content).toContain("- [ ] Review with stakeholders 📅 2025-11-05");
});

it("should use description directly when provided (no 'Original inbox item:' prefix)", async () => {
const result: GTDProcessingResult = {
isActionable: true,
category: "project",
projectOutcome: "My Project",
nextAction: "First step",
reasoning: "User created project directly",
description: "This is a custom project description",
recommendedAction: "create-project",
recommendedActionReasoning: "User created",
};

(mockVault.getAbstractFileByPath as jest.Mock).mockImplementation((path: string) => {
if (path === "Projects") {
return {};
}
return null;
});
(mockVault.create as jest.Mock).mockResolvedValue({} as TFile);

await fileWriter.createProject(result, "original inbox text", ["work"]);

const [, content] = (mockVault.create as jest.Mock).mock.calls[0];
expect(content).toContain("This is a custom project description");
expect(content).not.toContain("Original inbox item:");
});

it("should use 'Original inbox item:' format when description is empty", async () => {
const result: GTDProcessingResult = {
isActionable: true,
category: "project",
projectOutcome: "My Project",
nextAction: "First step",
reasoning: "Some reasoning",
description: "",
recommendedAction: "create-project",
recommendedActionReasoning: "Multi-step",
};

(mockVault.getAbstractFileByPath as jest.Mock).mockImplementation((path: string) => {
if (path === "Projects") {
return {};
}
return null;
});
(mockVault.create as jest.Mock).mockResolvedValue({} as TFile);

await fileWriter.createProject(result, "call Bob about the project", ["work"]);

const [, content] = (mockVault.create as jest.Mock).mock.calls[0];
expect(content).toContain("Original inbox item: call Bob about the project");
});

it("should use 'Original inbox item:' format when description is undefined", async () => {
const result: GTDProcessingResult = {
isActionable: true,
category: "project",
projectOutcome: "My Project",
nextAction: "First step",
reasoning: "Some reasoning",
recommendedAction: "create-project",
recommendedActionReasoning: "Multi-step",
};

(mockVault.getAbstractFileByPath as jest.Mock).mockImplementation((path: string) => {
if (path === "Projects") {
return {};
}
return null;
});
(mockVault.create as jest.Mock).mockResolvedValue({} as TFile);

await fileWriter.createProject(result, "inbox item text", ["work"]);

const [, content] = (mockVault.create as jest.Mock).mock.calls[0];
expect(content).toContain("Original inbox item: inbox item text");
});
});

describe("addNextActionToProject", () => {
Expand Down
8 changes: 5 additions & 3 deletions tests/new-project-modal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ describe("NewProjectModal", () => {
expect.objectContaining({
projectOutcome: "Test Project",
nextAction: "First action",
reasoning: "Project description",
reasoning: "User created project directly",
description: "Project description",
projectPriority: 1,
}),
"Test Project",
Expand Down Expand Up @@ -203,7 +204,7 @@ describe("NewProjectModal", () => {
);
});

it("should use title as reasoning when description is empty", async () => {
it("should pass empty description when description is not provided", async () => {
await modal.onOpen();

const data = (modal as any).data;
Expand All @@ -219,7 +220,8 @@ describe("NewProjectModal", () => {

expect(writerInstance.createProject).toHaveBeenCalled();
const firstArg = writerInstance.createProject.mock.calls[0][0];
expect(firstArg.reasoning).toBe("Test Project");
expect(firstArg.reasoning).toBe("User created project directly");
expect(firstArg.description).toBe("");
});

it("should add action to focus when option is enabled", async () => {
Expand Down