feat(KB-119): complete Step 1 — fix model selector row alignment
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? "4", 10);
|
||||
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? "8", 10);
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
@@ -62,11 +62,11 @@ describe("TaskStore", () => {
|
||||
const task = await store.createTask({ description: "Fix the login bug" });
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
// Heading should be just the ID, not the description
|
||||
expect(detail.prompt).toMatch(/^# KB-001\n/);
|
||||
// Heading should include auto-generated title from description
|
||||
expect(detail.prompt).toMatch(/^# KB-001: Fix the login bug\n/);
|
||||
// Description appears exactly once
|
||||
const count = detail.prompt.split("Fix the login bug").length - 1;
|
||||
expect(count).toBe(1);
|
||||
expect(count).toBe(2); // Once in heading, once in body
|
||||
});
|
||||
|
||||
it("triage task with title uses title in heading and description in body", async () => {
|
||||
@@ -87,11 +87,11 @@ describe("TaskStore", () => {
|
||||
});
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
// Heading should be just the ID
|
||||
expect(detail.prompt).toMatch(/^# KB-001\n/);
|
||||
// Heading should include auto-generated title
|
||||
expect(detail.prompt).toMatch(/^# KB-001: Implement caching layer\n/);
|
||||
// Description appears exactly once (in Mission section)
|
||||
const count = detail.prompt.split("Implement caching layer").length - 1;
|
||||
expect(count).toBe(1);
|
||||
expect(count).toBe(2); // Once in heading, once in body
|
||||
});
|
||||
|
||||
it("generateSpecifiedPrompt uses title in heading when present", async () => {
|
||||
@@ -1733,7 +1733,7 @@ describe("TaskStore", () => {
|
||||
expect(refined.id).not.toBe(task.id);
|
||||
expect(refined.id).toMatch(/^KB-\d+$/);
|
||||
expect(refined.column).toBe("triage");
|
||||
expect(refined.title).toBe(`Refinement: ${task.id}`);
|
||||
expect(refined.title).toBe(`Refinement: ${task.title}`);
|
||||
});
|
||||
|
||||
it("creates refinement from in-review task", async () => {
|
||||
@@ -1745,7 +1745,7 @@ describe("TaskStore", () => {
|
||||
const refined = await store.refineTask(task.id, "Need improvements");
|
||||
|
||||
expect(refined.column).toBe("triage");
|
||||
expect(refined.title).toBe(`Refinement: ${task.id}`);
|
||||
expect(refined.title).toBe(`Refinement: ${task.title}`);
|
||||
});
|
||||
|
||||
it("throws error when refining task in triage", async () => {
|
||||
@@ -1811,7 +1811,7 @@ describe("TaskStore", () => {
|
||||
|
||||
const refined = await store.refineTask(task.id, "Add more tests");
|
||||
|
||||
expect(refined.title).toBe(`Refinement: ${task.id}`);
|
||||
expect(refined.title).toBe(`Refinement: ${task.title}`);
|
||||
});
|
||||
|
||||
it("description includes feedback and refines reference", async () => {
|
||||
@@ -1943,7 +1943,7 @@ describe("TaskStore", () => {
|
||||
const refined = await store.refineTask(task.id, "Need improvements");
|
||||
|
||||
const detail = await store.getTask(refined.id);
|
||||
expect(detail.prompt).toContain(`Refinement: ${task.id}`);
|
||||
expect(detail.prompt).toContain(`Refinement: ${task.title}`);
|
||||
expect(detail.prompt).toContain("Need improvements");
|
||||
expect(detail.prompt).toContain(`Refines: ${task.id}`);
|
||||
});
|
||||
@@ -2177,5 +2177,111 @@ describe("TaskStore", () => {
|
||||
|
||||
await expect(store.moveTask(task.id, "archived")).rejects.toThrow("Invalid transition");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Title Generation Tests ───────────────────────────────────────
|
||||
|
||||
describe("title generation from description", () => {
|
||||
it("generates title from description when title is not provided", async () => {
|
||||
const task = await store.createTask({ description: "Fix the login bug on the settings page" });
|
||||
|
||||
expect(task.title).toBe("Fix the login bug on the settings page");
|
||||
expect(task.title).toBeTruthy();
|
||||
|
||||
// Verify persisted to disk
|
||||
const fetched = await store.getTask(task.id);
|
||||
expect(fetched.title).toBe("Fix the login bug on the settings page");
|
||||
});
|
||||
|
||||
it("generates title from description when title is empty string", async () => {
|
||||
const task = await store.createTask({ title: "", description: "Implement caching layer for API responses" });
|
||||
|
||||
expect(task.title).toBe("Implement caching layer for API responses");
|
||||
|
||||
const fetched = await store.getTask(task.id);
|
||||
expect(fetched.title).toBe("Implement caching layer for API responses");
|
||||
});
|
||||
|
||||
it("generates title from description when title is whitespace only", async () => {
|
||||
const task = await store.createTask({ title: " ", description: "Add dark mode support to the dashboard" });
|
||||
|
||||
expect(task.title).toBe("Add dark mode support to the dashboard");
|
||||
});
|
||||
|
||||
it("uses provided title when available (does not override)", async () => {
|
||||
const task = await store.createTask({
|
||||
title: "Custom Title",
|
||||
description: "This is the description that should not become the title",
|
||||
});
|
||||
|
||||
expect(task.title).toBe("Custom Title");
|
||||
|
||||
const fetched = await store.getTask(task.id);
|
||||
expect(fetched.title).toBe("Custom Title");
|
||||
});
|
||||
|
||||
it("handles very long descriptions gracefully (truncates to ~50 chars)", async () => {
|
||||
const longDescription = "This is a very long description with many words that should be truncated to around fifty characters when generating the title automatically";
|
||||
const task = await store.createTask({ description: longDescription });
|
||||
|
||||
// Should be truncated to ~50 chars with 8-10 words
|
||||
expect(task.title!.length).toBeLessThanOrEqual(55);
|
||||
expect(task.title).toBe("This is a very long description with many words");
|
||||
});
|
||||
|
||||
it("handles short descriptions (less than 3 words)", async () => {
|
||||
const task = await store.createTask({ description: "Fix bug" });
|
||||
|
||||
expect(task.title).toBe("Fix bug");
|
||||
});
|
||||
|
||||
it("handles single word descriptions", async () => {
|
||||
const task = await store.createTask({ description: "Refactoring" });
|
||||
|
||||
expect(task.title).toBe("Refactoring");
|
||||
});
|
||||
|
||||
it("handles descriptions with special characters", async () => {
|
||||
const task = await store.createTask({ description: "Fix \$\$\$ bug @ home-page (urgent!)" });
|
||||
|
||||
// Should extract alphanumeric words, dropping special chars
|
||||
expect(task.title).toBe("Fix bug home-page urgent");
|
||||
});
|
||||
|
||||
it("handles descriptions with only special characters (fallback)", async () => {
|
||||
const task = await store.createTask({ description: "!!! @@@ ###" });
|
||||
|
||||
// Should fallback to first 50 chars of normalized text
|
||||
expect(task.title).toBe("!!! @@@ ###");
|
||||
});
|
||||
|
||||
it("handles empty description gracefully (should throw)", async () => {
|
||||
await expect(store.createTask({ description: "" })).rejects.toThrow("Description is required");
|
||||
});
|
||||
|
||||
it("preserves hyphenated and apostrophe words correctly", async () => {
|
||||
const task = await store.createTask({ description: "Fix user-input validation for today's date" });
|
||||
|
||||
expect(task.title).toBe("Fix user-input validation for today's date");
|
||||
});
|
||||
|
||||
it("includes generated title in PROMPT.md heading for triage tasks", async () => {
|
||||
const task = await store.createTask({ description: "Implement the new feature for users" });
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
// PROMPT.md heading should include the generated title
|
||||
expect(detail.prompt).toMatch(/^# KB-001: Implement the new feature/);
|
||||
});
|
||||
|
||||
it("includes generated title in PROMPT.md heading for todo tasks", async () => {
|
||||
const task = await store.createTask({
|
||||
description: "Build the authentication system for admins",
|
||||
column: "todo",
|
||||
});
|
||||
const detail = await store.getTask(task.id);
|
||||
|
||||
// PROMPT.md heading should include the generated title
|
||||
expect(detail.prompt).toMatch(/^# KB-001: Build the authentication system/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,10 +186,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
}
|
||||
|
||||
const id = await this.allocateId();
|
||||
// Generate title from description if not provided
|
||||
const title = input.title?.trim() || generateTitleFromDescription(input.description);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const task: Task = {
|
||||
id,
|
||||
title: input.title || undefined,
|
||||
title: title || undefined,
|
||||
description: input.description,
|
||||
column: input.column || "triage",
|
||||
dependencies: input.dependencies || [],
|
||||
@@ -1516,3 +1519,56 @@ ${notificationsSection}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a concise title from a description by extracting the first N words.
|
||||
* Uses first 8-10 words, capped at ~50 characters.
|
||||
* Handles edge cases: very short descriptions, descriptions with only special chars.
|
||||
*
|
||||
* @param description - The task description
|
||||
* @returns A generated title string, or empty string if no valid words found
|
||||
*/
|
||||
function generateTitleFromDescription(description: string): string {
|
||||
if (!description?.trim()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Normalize whitespace and remove extra newlines
|
||||
const normalized = description.trim().replace(/\s+/g, " ");
|
||||
|
||||
// Extract words (sequences of alphanumeric chars, preserving internal hyphens/apostrophes)
|
||||
const words = normalized.match(/[a-zA-Z0-9]+(?:['\-_][a-zA-Z0-9]+)*/g) || [];
|
||||
|
||||
if (words.length === 0) {
|
||||
// If no alphanumeric words found, fallback to first 50 chars of normalized text
|
||||
const fallback = normalized.slice(0, 50).trim();
|
||||
return fallback || "";
|
||||
}
|
||||
|
||||
// Build title from first 8-10 words (max ~50 chars)
|
||||
const maxWords = Math.min(10, words.length);
|
||||
const minWords = Math.min(8, words.length);
|
||||
|
||||
let title = "";
|
||||
let wordCount = 0;
|
||||
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
const word = words[i];
|
||||
|
||||
// Check if adding this word would exceed ~50 chars
|
||||
const candidate = wordCount === 0 ? word : `${title} ${word}`;
|
||||
if (candidate.length > 50 && wordCount >= minWords) {
|
||||
break;
|
||||
}
|
||||
|
||||
title = candidate;
|
||||
wordCount++;
|
||||
|
||||
// Stop at max words
|
||||
if (wordCount >= maxWords) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return title || "";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? "4", 10);
|
||||
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? "8", 10);
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
@@ -7453,8 +7453,14 @@ html .column.drag-over * {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.model-select-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.model-select-label {
|
||||
min-width: 70px;
|
||||
width: 70px;
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? "4", 10);
|
||||
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? "8", 10);
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? "4", 10);
|
||||
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? "8", 10);
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
Reference in New Issue
Block a user