mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
fix: add new project includes to the bottom instead of the top (#77)
* fix: add new project includes to the bottom instead of the top Previously, `buildEditorText` placed `include:` lines at the top of the editor text, above the prompt. This meant clicking "Add directory" would insert the new include above any text the user had already typed. Now includes are placed at the bottom of the editor as a trailing block, and `parseEditorText` collects them from the bottom instead of the top. This feels more natural — the user's prompt text stays at the top and directory includes accumulate below it. No migration needed: workingDirs and prompt are stored separately in project.json, and `buildEditorText` reconstructs the editor text in the new layout when the dialog opens. * fix: parse project includes anywhere in prompt editor
This commit is contained in:
@@ -19,38 +19,56 @@ describe("projectPromptText", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps blank lines between include lines and prompt content", () => {
|
||||
it("places includes after the prompt text", () => {
|
||||
expect(buildEditorText(["/tmp/one"], "My prompt")).toBe(
|
||||
"My prompt\n\ninclude: /tmp/one",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles includes-only text (no prompt)", () => {
|
||||
expect(buildEditorText(["/tmp/one", "/tmp/two"], "")).toBe(
|
||||
"include: /tmp/one\ninclude: /tmp/two",
|
||||
);
|
||||
});
|
||||
|
||||
it("collects include lines from anywhere in the editor text", () => {
|
||||
expect(
|
||||
parseEditorText("include: /tmp/one\n\ninclude: /tmp/two\nprompt"),
|
||||
parseEditorText("include: /tmp/one\nprompt\n\ninclude: /tmp/two"),
|
||||
).toEqual({
|
||||
prompt: "prompt",
|
||||
workingDirs: ["/tmp/one", "/tmp/two"],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps include-looking prompt lines after prompt content begins", () => {
|
||||
it("treats trailing include lines as working directories when saving", () => {
|
||||
expect(parseEditorText("prompt first\ninclude: /tmp/kept")).toEqual({
|
||||
prompt: "prompt first\ninclude: /tmp/kept",
|
||||
workingDirs: [],
|
||||
prompt: "prompt first",
|
||||
workingDirs: ["/tmp/kept"],
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a leading include block for prompt-only text", () => {
|
||||
it("creates a trailing include block for prompt-only text", () => {
|
||||
expect(insertWorkingDir("Existing prompt", "/tmp/one")).toBe(
|
||||
"include: /tmp/one\n\nExisting prompt",
|
||||
"Existing prompt\n\ninclude: /tmp/one",
|
||||
);
|
||||
});
|
||||
|
||||
it("appends a directory within the leading include block", () => {
|
||||
it("appends a directory to the trailing include block", () => {
|
||||
expect(
|
||||
insertWorkingDir("include: /tmp/one\n\nPrompt body", "/tmp/two"),
|
||||
).toBe("include: /tmp/one\ninclude: /tmp/two\n\nPrompt body");
|
||||
insertWorkingDir("Prompt body\n\ninclude: /tmp/one", "/tmp/two"),
|
||||
).toBe("Prompt body\n\ninclude: /tmp/one\ninclude: /tmp/two");
|
||||
});
|
||||
|
||||
it("adds a new directory to the bottom without moving existing prompt text", () => {
|
||||
expect(insertWorkingDir("include: /tmp/one\nPrompt body", "/tmp/two")).toBe(
|
||||
"include: /tmp/one\nPrompt body\n\ninclude: /tmp/two",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats tilde and absolute paths as equivalent when checking duplicates", () => {
|
||||
expect(
|
||||
hasEquivalentWorkingDir(
|
||||
"include: ~/dev/goose2\n\nPrompt body",
|
||||
"Prompt body\ninclude: ~/dev/goose2",
|
||||
"/Users/mtoohey/dev/goose2",
|
||||
"/Users/mtoohey",
|
||||
),
|
||||
@@ -60,7 +78,7 @@ describe("projectPromptText", () => {
|
||||
it("does not match different directories when checking duplicates", () => {
|
||||
expect(
|
||||
hasEquivalentWorkingDir(
|
||||
"include: ~/dev/goose2\n\nPrompt body",
|
||||
"Prompt body\ninclude: ~/dev/goose2",
|
||||
"/Users/mtoohey/dev/other",
|
||||
"/Users/mtoohey",
|
||||
),
|
||||
@@ -70,7 +88,7 @@ describe("projectPromptText", () => {
|
||||
it("treats trailing slashes as equivalent when checking duplicates", () => {
|
||||
expect(
|
||||
hasEquivalentWorkingDir(
|
||||
"include: /Users/mtoohey/dev/goose2/\n\nPrompt body",
|
||||
"Prompt body\ninclude: /Users/mtoohey/dev/goose2/",
|
||||
"/Users/mtoohey/dev/goose2",
|
||||
"/Users/mtoohey",
|
||||
),
|
||||
|
||||
@@ -4,12 +4,13 @@ import { INCLUDE_RE } from "./includePattern";
|
||||
export function buildEditorText(workingDirs: string[], prompt: string): string {
|
||||
const includeLines = workingDirs.map((directory) => `include: ${directory}`);
|
||||
if (includeLines.length === 0) return prompt;
|
||||
return [...includeLines, "", prompt].join("\n");
|
||||
if (prompt === "") return includeLines.join("\n");
|
||||
return [prompt, "", ...includeLines].join("\n");
|
||||
}
|
||||
|
||||
/** Parse editor text into { prompt, workingDirs }.
|
||||
* Only `include:` lines at the top of the text (before the first
|
||||
* non-include, non-blank line) are treated as working directories. */
|
||||
* `include:` lines can appear anywhere in the editor text and are
|
||||
* extracted into workingDirs when the project is saved. */
|
||||
export function parseEditorText(text: string): {
|
||||
prompt: string;
|
||||
workingDirs: string[];
|
||||
@@ -17,26 +18,21 @@ export function parseEditorText(text: string): {
|
||||
const lines = text.split("\n");
|
||||
const workingDirs: string[] = [];
|
||||
const promptLines: string[] = [];
|
||||
let parsingIncludeBlock = true;
|
||||
|
||||
for (const line of lines) {
|
||||
const match = line.match(INCLUDE_RE);
|
||||
if (parsingIncludeBlock && match) {
|
||||
if (match) {
|
||||
workingDirs.push(match[1].trim());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.trim() !== "") {
|
||||
parsingIncludeBlock = false;
|
||||
}
|
||||
|
||||
promptLines.push(line);
|
||||
}
|
||||
|
||||
// Trim leading/trailing blank lines from the prompt.
|
||||
while (promptLines[0]?.trim() === "") {
|
||||
promptLines.shift();
|
||||
}
|
||||
|
||||
while (promptLines[promptLines.length - 1]?.trim() === "") {
|
||||
promptLines.pop();
|
||||
}
|
||||
@@ -48,12 +44,17 @@ export function parseEditorText(text: string): {
|
||||
}
|
||||
|
||||
export function insertWorkingDir(text: string, directory: string): string {
|
||||
if (text === "") {
|
||||
const trimmedText = text.trimEnd();
|
||||
|
||||
if (trimmedText === "") {
|
||||
return `include: ${directory}`;
|
||||
}
|
||||
|
||||
const { prompt, workingDirs } = parseEditorText(text);
|
||||
return buildEditorText([...workingDirs, directory], prompt);
|
||||
const trimmedLines = trimmedText.split("\n");
|
||||
const lastLine = trimmedLines[trimmedLines.length - 1] ?? "";
|
||||
const separator = lastLine.match(INCLUDE_RE) ? "\n" : "\n\n";
|
||||
|
||||
return `${trimmedText}${separator}include: ${directory}`;
|
||||
}
|
||||
|
||||
export function hasEquivalentWorkingDir(
|
||||
|
||||
Reference in New Issue
Block a user