feat: productivity suite — task loop, workflow, theming, docs & more (#197)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m39s
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
CI / Lint & Test (push) Has been cancelled

## 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:
2026-03-07 03:08:33 -08:00
committed by Naomi Carrigan
parent 1ae440659c
commit e6e9f7ae59
52 changed files with 8865 additions and 529 deletions
+106 -9
View File
@@ -158,6 +158,16 @@ pub struct HikariConfig {
#[serde(default)]
pub custom_ui_font_family: Option<String>,
// Task Loop auto-commit settings
#[serde(default)]
pub task_loop_auto_commit: bool,
#[serde(default = "default_task_loop_commit_prefix")]
pub task_loop_commit_prefix: String,
#[serde(default)]
pub task_loop_include_summary: bool,
}
impl Default for HikariConfig {
@@ -201,6 +211,9 @@ impl Default for HikariConfig {
custom_font_family: None,
custom_ui_font_path: None,
custom_ui_font_family: None,
task_loop_auto_commit: false,
task_loop_commit_prefix: "feat".to_string(),
task_loop_include_summary: false,
}
}
}
@@ -241,6 +254,10 @@ fn default_background_image_opacity() -> f32 {
0.3
}
fn default_task_loop_commit_prefix() -> String {
"feat".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum BudgetAction {
@@ -258,6 +275,18 @@ pub enum Theme {
#[serde(rename = "high-contrast")]
HighContrast,
Custom,
Dracula,
Catppuccin,
Nord,
Solarized,
#[serde(rename = "solarized-light")]
SolarizedLight,
#[serde(rename = "catppuccin-latte")]
CatppuccinLatte,
#[serde(rename = "gruvbox-light")]
GruvboxLight,
#[serde(rename = "rose-pine-dawn")]
RosePineDawn,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
@@ -320,6 +349,9 @@ mod tests {
assert!(config.custom_font_family.is_none());
assert!(config.custom_ui_font_path.is_none());
assert!(config.custom_ui_font_family.is_none());
assert!(!config.task_loop_auto_commit);
assert_eq!(config.task_loop_commit_prefix, "feat");
assert!(!config.task_loop_include_summary);
}
#[test]
@@ -363,6 +395,9 @@ mod tests {
custom_font_family: Some("MyFont".to_string()),
custom_ui_font_path: None,
custom_ui_font_family: None,
task_loop_auto_commit: true,
task_loop_commit_prefix: "fix".to_string(),
task_loop_include_summary: true,
};
let json = serde_json::to_string(&config).unwrap();
@@ -377,22 +412,84 @@ mod tests {
deserialized.greeting_custom_prompt,
Some("Hello!".to_string())
);
assert!(deserialized.task_loop_auto_commit);
assert_eq!(deserialized.task_loop_commit_prefix, "fix");
assert!(deserialized.task_loop_include_summary);
}
#[test]
fn test_theme_serialization() {
let dark = Theme::Dark;
let light = Theme::Light;
let high_contrast = Theme::HighContrast;
assert_eq!(serde_json::to_string(&dark).unwrap(), "\"dark\"");
assert_eq!(serde_json::to_string(&light).unwrap(), "\"light\"");
assert_eq!(serde_json::to_string(&Theme::Dark).unwrap(), "\"dark\"");
assert_eq!(serde_json::to_string(&Theme::Light).unwrap(), "\"light\"");
assert_eq!(
serde_json::to_string(&high_contrast).unwrap(),
serde_json::to_string(&Theme::HighContrast).unwrap(),
"\"high-contrast\""
);
assert_eq!(serde_json::to_string(&Theme::Custom).unwrap(), "\"custom\"");
assert_eq!(
serde_json::to_string(&Theme::Dracula).unwrap(),
"\"dracula\""
);
assert_eq!(
serde_json::to_string(&Theme::Catppuccin).unwrap(),
"\"catppuccin\""
);
assert_eq!(serde_json::to_string(&Theme::Nord).unwrap(), "\"nord\"");
assert_eq!(
serde_json::to_string(&Theme::Solarized).unwrap(),
"\"solarized\""
);
assert_eq!(
serde_json::to_string(&Theme::SolarizedLight).unwrap(),
"\"solarized-light\""
);
assert_eq!(
serde_json::to_string(&Theme::CatppuccinLatte).unwrap(),
"\"catppuccin-latte\""
);
assert_eq!(
serde_json::to_string(&Theme::GruvboxLight).unwrap(),
"\"gruvbox-light\""
);
assert_eq!(
serde_json::to_string(&Theme::RosePineDawn).unwrap(),
"\"rose-pine-dawn\""
);
}
let custom = Theme::Custom;
assert_eq!(serde_json::to_string(&custom).unwrap(), "\"custom\"");
#[test]
fn test_theme_deserialization() {
assert_eq!(
serde_json::from_str::<Theme>("\"dracula\"").unwrap(),
Theme::Dracula
);
assert_eq!(
serde_json::from_str::<Theme>("\"catppuccin\"").unwrap(),
Theme::Catppuccin
);
assert_eq!(
serde_json::from_str::<Theme>("\"nord\"").unwrap(),
Theme::Nord
);
assert_eq!(
serde_json::from_str::<Theme>("\"solarized\"").unwrap(),
Theme::Solarized
);
assert_eq!(
serde_json::from_str::<Theme>("\"solarized-light\"").unwrap(),
Theme::SolarizedLight
);
assert_eq!(
serde_json::from_str::<Theme>("\"catppuccin-latte\"").unwrap(),
Theme::CatppuccinLatte
);
assert_eq!(
serde_json::from_str::<Theme>("\"gruvbox-light\"").unwrap(),
Theme::GruvboxLight
);
assert_eq!(
serde_json::from_str::<Theme>("\"rose-pine-dawn\"").unwrap(),
Theme::RosePineDawn
);
}
}