| 1 | //! The eight Codewhale work-dock panels, with ordinary example rows. |
| 2 | |
| 3 | use ratatui::{buffer::Buffer, layout::Rect, text::Line}; |
| 4 | |
| 5 | use super::Entry; |
| 6 | use crate::{ |
| 7 | Paint, Role, Theme, Workbar, WorkbarAgent, WorkbarPanel, WorkbarPlacement, WorkbarRow, |
| 8 | WorkbarTab, WorkbarTone, |
| 9 | }; |
| 10 | |
| 11 | fn tabs() -> Vec<WorkbarTab> { |
| 12 | WorkbarPanel::ORDER |
| 13 | .into_iter() |
| 14 | .map(|panel| match panel { |
| 15 | WorkbarPanel::Tasks => WorkbarTab::new(panel).count(3), |
| 16 | WorkbarPanel::Fleet => WorkbarTab::new(panel).count(2), |
| 17 | WorkbarPanel::Jobs => WorkbarTab::new(panel).count(1), |
| 18 | WorkbarPanel::Files => WorkbarTab::new(panel).count(3), |
| 19 | WorkbarPanel::Notes => WorkbarTab::new(panel).count(2), |
| 20 | _ => WorkbarTab::new(panel), |
| 21 | }) |
| 22 | .collect() |
| 23 | } |
| 24 | |
| 25 | fn rows(panel: WorkbarPanel) -> Vec<WorkbarRow> { |
| 26 | let dot = "·"; |
| 27 | match panel { |
| 28 | WorkbarPanel::Tasks => vec![ |
| 29 | WorkbarRow::new("graph:read", "Read the existing component") |
| 30 | .mark("✓") |
| 31 | .tone(WorkbarTone::Success) |
| 32 | .detail("completed"), |
| 33 | WorkbarRow::new("graph:extract", "Extract the native work dock") |
| 34 | .mark("●") |
| 35 | .tone(WorkbarTone::Live) |
| 36 | .detail("in progress"), |
| 37 | WorkbarRow::new("graph:document", "Document the reusable API") |
| 38 | .mark("○") |
| 39 | .detail("pending"), |
| 40 | ], |
| 41 | WorkbarPanel::Fleet => vec![ |
| 42 | WorkbarRow::new("worker:build", "Builder").mark("●").agent( |
| 43 | WorkbarAgent::new( |
| 44 | "builder", |
| 45 | "running", |
| 46 | "Extract the dock and keep the native layout", |
| 47 | ) |
| 48 | .elapsed_seconds(754) |
| 49 | .tokens(111_900) |
| 50 | .remaining(2), |
| 51 | ), |
| 52 | WorkbarRow::new("worker:review", "Reviewer") |
| 53 | .mark("✓") |
| 54 | .agent( |
| 55 | WorkbarAgent::new( |
| 56 | "reviewer", |
| 57 | "completed", |
| 58 | "Review tab and keyboard navigation", |
| 59 | ) |
| 60 | .elapsed_seconds(183), |
| 61 | ), |
| 62 | ], |
| 63 | WorkbarPanel::Jobs => vec![ |
| 64 | WorkbarRow::new("shell:gallery", "Render the component gallery") |
| 65 | .mark("●") |
| 66 | .tone(WorkbarTone::Live) |
| 67 | .detail("running"), |
| 68 | WorkbarRow::new("shell:check", "Check formatting") |
| 69 | .mark("✓") |
| 70 | .tone(WorkbarTone::Success) |
| 71 | .detail("completed"), |
| 72 | ], |
| 73 | WorkbarPanel::Files => vec![ |
| 74 | WorkbarRow::new("files:edited", "Edited 2") |
| 75 | .mark("▾") |
| 76 | .tone(WorkbarTone::Heading) |
| 77 | .selectable(false), |
| 78 | WorkbarRow::new("files:edit:workbar", "src/components/workbar.rs") |
| 79 | .mark("✎") |
| 80 | .tone(WorkbarTone::Success) |
| 81 | .detail("+48 -12"), |
| 82 | WorkbarRow::new("files:edit:gallery", "src/gallery/workbar.rs") |
| 83 | .mark("✎") |
| 84 | .tone(WorkbarTone::Success) |
| 85 | .detail("+22 -4"), |
| 86 | WorkbarRow::new("files:read", "Read 1") |
| 87 | .mark("▾") |
| 88 | .tone(WorkbarTone::Heading) |
| 89 | .selectable(false), |
| 90 | WorkbarRow::new("files:read:source", "work_surface/render/mod.rs") |
| 91 | .mark("·") |
| 92 | .selectable(false), |
| 93 | ], |
| 94 | WorkbarPanel::Notes => vec![ |
| 95 | WorkbarRow::new("notes:1", "Keep the native dock tab order") |
| 96 | .mark("▪") |
| 97 | .tone(WorkbarTone::Live) |
| 98 | .detail("/note show 1"), |
| 99 | WorkbarRow::new("notes:2", "Make each row independently selectable") |
| 100 | .mark("▪") |
| 101 | .tone(WorkbarTone::Live) |
| 102 | .detail("/note show 2"), |
| 103 | ], |
| 104 | WorkbarPanel::Context => vec![ |
| 105 | WorkbarRow::new( |
| 106 | "context:budget", |
| 107 | format!("48k of 128k {dot} 38% {dot} compacts at 90%"), |
| 108 | ) |
| 109 | .mark("◔") |
| 110 | .tone(WorkbarTone::Live) |
| 111 | .detail("/context for the full source map"), |
| 112 | WorkbarRow::new( |
| 113 | "context:system", |
| 114 | format!("system + tools {dot} 8.4k {dot} 24 tools"), |
| 115 | ) |
| 116 | .mark("·") |
| 117 | .selectable(false), |
| 118 | WorkbarRow::new( |
| 119 | "context:conversation", |
| 120 | format!("conversation {dot} 18 {dot} 29.2k"), |
| 121 | ) |
| 122 | .mark("·") |
| 123 | .selectable(false), |
| 124 | WorkbarRow::new("context:tool-output", format!("tool output {dot} 10.4k")) |
| 125 | .mark("·") |
| 126 | .selectable(false), |
| 127 | WorkbarRow::new("context:compact", "compact now") |
| 128 | .mark("▸") |
| 129 | .tone(WorkbarTone::Live) |
| 130 | .detail("/compact"), |
| 131 | ], |
| 132 | WorkbarPanel::Git => vec![ |
| 133 | WorkbarRow::new("git:branch", format!("feature/work-dock {dot} up to date")) |
| 134 | .mark("⎇") |
| 135 | .tone(WorkbarTone::Live) |
| 136 | .detail("example/component-kit"), |
| 137 | WorkbarRow::new("git:changes", "2 modified files") |
| 138 | .mark("±") |
| 139 | .tone(WorkbarTone::Live) |
| 140 | .detail("/diff"), |
| 141 | WorkbarRow::new("git:commit", "Extract reusable work dock") |
| 142 | .mark("·") |
| 143 | .detail("latest commit"), |
| 144 | ], |
| 145 | WorkbarPanel::Cost => vec![ |
| 146 | WorkbarRow::new("price:session", format!("session {dot} $0.42")) |
| 147 | .mark("$") |
| 148 | .tone(WorkbarTone::Live) |
| 149 | .detail("/cost for the full ledger"), |
| 150 | WorkbarRow::new("price:agents", format!("agents {dot} $0.18")) |
| 151 | .mark("·") |
| 152 | .detail("2 of 2 agents priced") |
| 153 | .selectable(false), |
| 154 | WorkbarRow::new( |
| 155 | "price:cache", |
| 156 | format!("cache hit {dot} parent 72% {dot} agents 64%"), |
| 157 | ) |
| 158 | .mark("·") |
| 159 | .detail("/cache for per-turn cache telemetry"), |
| 160 | ], |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | pub(crate) fn sample(panel: WorkbarPanel) -> Workbar { |
| 165 | let rows = rows(panel); |
| 166 | let selected = rows |
| 167 | .iter() |
| 168 | .find(|row| row.selectable) |
| 169 | .map(|row| row.id.clone()); |
| 170 | let mut dock = Workbar::new(panel, rows).tabs(tabs()).focused(true); |
| 171 | if let Some(selected) = selected { |
| 172 | dock = dock.selected(selected); |
| 173 | } |
| 174 | if panel == WorkbarPanel::Tasks { |
| 175 | let dot = "·"; |
| 176 | dock = dock |
| 177 | .goal("Make the work dock reusable") |
| 178 | .progress(format!("TODO {dot} 1/3 {dot} 2 left")); |
| 179 | } |
| 180 | dock |
| 181 | } |
| 182 | |
| 183 | fn tasks(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 184 | sample(WorkbarPanel::Tasks).paint(area, buf, theme); |
| 185 | } |
| 186 | fn fleet(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 187 | sample(WorkbarPanel::Fleet).paint(area, buf, theme); |
| 188 | } |
| 189 | fn jobs(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 190 | sample(WorkbarPanel::Jobs).paint(area, buf, theme); |
| 191 | } |
| 192 | fn files(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 193 | sample(WorkbarPanel::Files).paint(area, buf, theme); |
| 194 | } |
| 195 | fn notes(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 196 | sample(WorkbarPanel::Notes).paint(area, buf, theme); |
| 197 | } |
| 198 | fn context(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 199 | sample(WorkbarPanel::Context).paint(area, buf, theme); |
| 200 | } |
| 201 | fn git(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 202 | sample(WorkbarPanel::Git).paint(area, buf, theme); |
| 203 | } |
| 204 | fn cost(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 205 | sample(WorkbarPanel::Cost).paint(area, buf, theme); |
| 206 | } |
| 207 | fn top(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 208 | sample(WorkbarPanel::Tasks) |
| 209 | .placement(WorkbarPlacement::Top) |
| 210 | .paint(area, buf, theme); |
| 211 | } |
| 212 | |
| 213 | fn narrow(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 214 | let rows = (0..12) |
| 215 | .map(|index| { |
| 216 | WorkbarRow::new(format!("graph:{index}"), format!("Plan step {}", index + 1)) |
| 217 | .mark(if index == 3 { "●" } else { "○" }) |
| 218 | .tone(if index == 3 { |
| 219 | WorkbarTone::Live |
| 220 | } else { |
| 221 | WorkbarTone::Muted |
| 222 | }) |
| 223 | }) |
| 224 | .collect(); |
| 225 | Workbar::new(WorkbarPanel::Tasks, rows) |
| 226 | .tabs(tabs()) |
| 227 | .goal("Extract the native dock") |
| 228 | .progress(if theme.ascii() { |
| 229 | "TODO . 3/12 . 9 left" |
| 230 | } else { |
| 231 | "TODO · 3/12 · 9 left" |
| 232 | }) |
| 233 | .focused(true) |
| 234 | .selected("graph:3") |
| 235 | .offset(3) |
| 236 | .paint(area, buf, theme); |
| 237 | } |
| 238 | |
| 239 | fn side_with(placement: WorkbarPlacement, area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 240 | let area = area.intersection(buf.area); |
| 241 | let dock = sample(WorkbarPanel::Fleet).placement(placement); |
| 242 | let regions = dock.regions(area); |
| 243 | if regions.content.height > 0 && regions.content.width > 0 { |
| 244 | buf.set_line( |
| 245 | regions.content.x, |
| 246 | regions.content.y, |
| 247 | &Line::styled("Codewhale", theme.fg(Role::Foreground)), |
| 248 | regions.content.width, |
| 249 | ); |
| 250 | if regions.content.height > 2 { |
| 251 | buf.set_line( |
| 252 | regions.content.x, |
| 253 | regions.content.y + 2, |
| 254 | &Line::styled("The conversation keeps its space.", theme.fg(Role::Muted)), |
| 255 | regions.content.width, |
| 256 | ); |
| 257 | } |
| 258 | } |
| 259 | if let Some(area) = regions.dock { |
| 260 | dock.placement(regions.placement).paint(area, buf, theme); |
| 261 | } |
| 262 | } |
| 263 | fn left(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 264 | side_with(WorkbarPlacement::Left, area, buf, theme); |
| 265 | } |
| 266 | fn right(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 267 | side_with(WorkbarPlacement::Right, area, buf, theme); |
| 268 | } |
| 269 | |
| 270 | pub(crate) fn entries() -> Vec<Entry> { |
| 271 | [ |
| 272 | ( |
| 273 | "workbar-tasks", |
| 274 | 104, |
| 275 | 7, |
| 276 | tasks as fn(Rect, &mut Buffer, &Theme), |
| 277 | ), |
| 278 | ("workbar-fleet", 104, 5, fleet), |
| 279 | ("workbar-jobs", 104, 5, jobs), |
| 280 | ("workbar-files", 104, 8, files), |
| 281 | ("workbar-notes", 104, 5, notes), |
| 282 | ("workbar-context", 104, 8, context), |
| 283 | ("workbar-git", 104, 6, git), |
| 284 | ("workbar-cost", 104, 6, cost), |
| 285 | ("workbar-top", 104, 7, top), |
| 286 | ("workbar-narrow", 38, 8, narrow), |
| 287 | ("workbar-left", 104, 12, left), |
| 288 | ("workbar-right", 104, 12, right), |
| 289 | ] |
| 290 | .into_iter() |
| 291 | .map(|(name, width, height, draw)| Entry { |
| 292 | name, |
| 293 | width, |
| 294 | height, |
| 295 | draw, |
| 296 | }) |
| 297 | .collect() |
| 298 | } |
| 299 |