generated from nhcarrigan/template
feat: productivity suite — task loop, workflow, theming, docs & more (#197)
## Summary A large productivity-focused feature branch delivering a suite of improvements across automation, project management, theming, performance, and documentation. ### Features - **Guided Project Workflow** (#189) — Four-phase workflow panel (Discuss → Plan → Execute → Verify) to keep projects structured from idea to completion - **Automated Task Loop** (#179) — Per-task conversation orchestration with wave-based parallel execution, blocked-task detection, and concurrency control - **Wave-Based Parallel Execution** (#191) — Tasks run in dependency-aware waves with configurable concurrency; independent tasks execute in parallel - **Auto-Commit After Task Completion** (#192) — Task Loop optionally commits after each completed task so progress is never lost - **PRD Creator** (#180) — AI-assisted PRD and task list panel that outputs `hikari-tasks.json` for the Task Loop to consume - **Project Context Panel** (#188) — Persistent `PROJECT.md`, `REQUIREMENTS.md`, `ROADMAP.md`, and `STATE.md` files injected into Claude's context automatically - **Codebase Mapper** (#190) — Generates a `CODEBASE.md` architectural summary so Claude always understands the project structure - **Community Preset Themes** (#181) — Six built-in community themes: Dracula, Catppuccin Mocha, Nord, Solarized Dark, Gruvbox Dark, and Rosé Pine - **In-App Changelog Panel** (#193) — Fetches release notes from GitHub at runtime and displays them inside the app - **Full Embedded Documentation** (#196) — Replaced the single-page help modal with a 12-page paginated docs browser featuring a sidebar TOC, prev/next navigation, keyboard navigation (arrow keys, `?` shortcut), and comprehensive coverage of every feature ### Performance & Fixes - **Lazy Loading & Virtualisation** (#194) — Virtual windowing for conversation history, markdown memoisation, and debounced search for smooth rendering of large sessions - **Ctrl+C Copy Fix** (#195) — `Ctrl+C` now copies selected text as expected; interrupt-Claude behaviour only fires when no text is selected ### UX - Back-to-workflow button in PRD Creator and Task Loop panels for easy navigation - Navigation icon cluster replaced with a single clean dropdown menu ## Closes Closes #179 Closes #180 Closes #181 Closes #188 Closes #189 Closes #190 Closes #191 Closes #192 Closes #193 Closes #194 Closes #195 Closes #196 --- ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #197 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #197.
This commit is contained in:
@@ -89,6 +89,27 @@ function truncateToolContent(content: string): string {
|
||||
return content.slice(0, TOOL_COLLAPSE_THRESHOLD) + "…";
|
||||
}
|
||||
|
||||
// Virtual windowing helpers — mirror the logic in Terminal.svelte
|
||||
|
||||
const WINDOW_SIZE = 150;
|
||||
const LOAD_CHUNK = 50;
|
||||
const AVG_LINE_HEIGHT = 60;
|
||||
|
||||
/** Returns the windowStart index when auto-scrolling to the bottom. */
|
||||
function autoScrollWindowStart(linesLength: number, windowSize: number): number {
|
||||
return Math.max(0, linesLength - windowSize);
|
||||
}
|
||||
|
||||
/** Returns the new windowStart after loading LOAD_CHUNK older messages. */
|
||||
function olderWindowStart(currentStart: number, chunkSize: number): number {
|
||||
return Math.max(0, currentStart - chunkSize);
|
||||
}
|
||||
|
||||
/** Returns the height in pixels of the invisible top spacer. */
|
||||
function topSpacerHeight(windowStart: number, avgLineHeight: number): number {
|
||||
return windowStart * avgLineHeight;
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
describe("getLineClass", () => {
|
||||
@@ -262,3 +283,86 @@ describe("truncateToolContent", () => {
|
||||
expect(result.endsWith("...")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("autoScrollWindowStart", () => {
|
||||
it("returns 0 when lines fit within the window", () => {
|
||||
expect(autoScrollWindowStart(50, WINDOW_SIZE)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 0 when lines exactly fill the window", () => {
|
||||
expect(autoScrollWindowStart(WINDOW_SIZE, WINDOW_SIZE)).toBe(0);
|
||||
});
|
||||
|
||||
it("advances when lines exceed the window size", () => {
|
||||
expect(autoScrollWindowStart(200, WINDOW_SIZE)).toBe(50);
|
||||
});
|
||||
|
||||
it("never returns a negative value", () => {
|
||||
expect(autoScrollWindowStart(0, WINDOW_SIZE)).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps last WINDOW_SIZE lines visible for large collections", () => {
|
||||
expect(autoScrollWindowStart(500, WINDOW_SIZE)).toBe(350);
|
||||
});
|
||||
});
|
||||
|
||||
describe("olderWindowStart", () => {
|
||||
it("subtracts the chunk size from the current start", () => {
|
||||
expect(olderWindowStart(100, LOAD_CHUNK)).toBe(50);
|
||||
});
|
||||
|
||||
it("never returns a negative value when chunk is larger than start", () => {
|
||||
expect(olderWindowStart(20, LOAD_CHUNK)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 0 when current start is 0", () => {
|
||||
expect(olderWindowStart(0, LOAD_CHUNK)).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 0 when current start exactly equals the chunk size", () => {
|
||||
expect(olderWindowStart(LOAD_CHUNK, LOAD_CHUNK)).toBe(0);
|
||||
});
|
||||
|
||||
it("correctly loads a partial chunk near the beginning", () => {
|
||||
expect(olderWindowStart(30, LOAD_CHUNK)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("topSpacerHeight", () => {
|
||||
it("returns 0 when windowStart is 0", () => {
|
||||
expect(topSpacerHeight(0, AVG_LINE_HEIGHT)).toBe(0);
|
||||
});
|
||||
|
||||
it("multiplies windowStart by avgLineHeight", () => {
|
||||
expect(topSpacerHeight(10, AVG_LINE_HEIGHT)).toBe(600);
|
||||
});
|
||||
|
||||
it("scales linearly with windowStart", () => {
|
||||
expect(topSpacerHeight(50, AVG_LINE_HEIGHT)).toBe(3000);
|
||||
expect(topSpacerHeight(100, AVG_LINE_HEIGHT)).toBe(6000);
|
||||
expect(topSpacerHeight(150, AVG_LINE_HEIGHT)).toBe(9000);
|
||||
});
|
||||
|
||||
it("uses the provided avgLineHeight rather than a hard-coded value", () => {
|
||||
expect(topSpacerHeight(5, 100)).toBe(500);
|
||||
expect(topSpacerHeight(5, 80)).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("virtual windowing constants", () => {
|
||||
it("WINDOW_SIZE is 150", () => {
|
||||
expect(WINDOW_SIZE).toBe(150);
|
||||
});
|
||||
|
||||
it("LOAD_CHUNK is 50", () => {
|
||||
expect(LOAD_CHUNK).toBe(50);
|
||||
});
|
||||
|
||||
it("LOAD_CHUNK is smaller than WINDOW_SIZE", () => {
|
||||
expect(LOAD_CHUNK).toBeLessThan(WINDOW_SIZE);
|
||||
});
|
||||
|
||||
it("AVG_LINE_HEIGHT is a positive number", () => {
|
||||
expect(AVG_LINE_HEIGHT).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user