返回 CodeWhale
scenes.rs
1 //! Complete Codewhale compositions made only from the public kit. Every
2 //! value is illustrative host data, not a live Engine/provider measurement.
3 use super::{Entry, workbench};
4 use crate::{
5 ApprovalCard, ApprovalChoice, ApprovalEffect, ApprovalKey, ApprovalKind, ApprovalScope,
6 ApprovalState, ApprovalSubject, Artifact, ArtifactKind, ArtifactShelf, AttentionItem,
7 AttentionQueue, ChoiceId, Composer, Cost, CountBar, Diff, DiffGutter, DiffWrap, Heading,
8 KeyHint, KeyHints, Message, Paint, PaneHeader, Receipt, ReceiptColumn, ReceiptTable,
9 ReceiptValue, ReviewKind, ReviewVerdict, Role, State, Theme, ToolCard, TreeNode, Whale,
10 WhaleState, WorkflowTree, WorkspaceFrame, parse_unified,
11 };
12 use ratatui::{buffer::Buffer, layout::Rect};
13 use std::time::Duration;
14
15 fn band(area: Rect, top: u16, height: u16) -> Rect {
16 let top = top.min(area.height);
17 Rect::new(
18 area.x,
19 area.y.saturating_add(top),
20 area.width,
21 height.min(area.height.saturating_sub(top)),
22 )
23 }
24 fn hints() -> KeyHints {
25 KeyHints::new(vec![
26 KeyHint::new("Enter", "send"),
27 KeyHint::new("Tab", "details"),
28 KeyHint::new("/", "commands"),
29 ])
30 }
31 fn composer(main: Rect, buf: &mut Buffer, theme: &Theme, draft: &'static str) {
32 let hints = hints();
33 let composer = Composer::new(draft).hints(&hints).focused(true);
34 let height = composer.height(main.width, theme).max(4).min(main.height);
35 let footer = main.height.saturating_sub(height.saturating_add(1));
36 workbench::context().paint(band(main, footer, 1), buf, theme);
37 composer.paint(band(main, footer.saturating_add(1), height), buf, theme);
38 }
39 fn files() -> Vec<Artifact<'static>> {
40 vec![
41 Artifact::new("WorkspaceFrame", ArtifactKind::File, State::Done)
42 .detail("src/components/workbench.rs")
43 .action("Open"),
44 Artifact::new("Habitat", ArtifactKind::File, State::Working)
45 .detail("src/components/habitat.rs")
46 .action("Open"),
47 Artifact::new("Visual review", ArtifactKind::Review, State::NeedsYou)
48 .detail("dark, light, narrow")
49 .action("Review"),
50 Artifact::new("Verification", ArtifactKind::Run, State::Done)
51 .detail("local component checks")
52 .receipt(
53 "Time",
54 ReceiptValue::Duration(Some(Duration::from_secs(12))),
55 ),
56 ]
57 }
58 fn workspace(area: Rect, buf: &mut Buffer, theme: &Theme) {
59 let area = area.intersection(buf.area);
60 let frame = WorkspaceFrame::new("codewhale-ratatui").branch("workspace-polish").mode("Work").footer(if area.width < 96 { "Illustrative / Tab opens files & decisions" } else { "Illustrative host data / Conversation + Files / Ctrl+K commands / Workbar summoned below" });
61 frame.paint(area, buf, theme);
62 let regions = frame.areas(area);
63 let main = regions.main;
64 PaneHeader::new("Make the terminal feel like Codewhale")
65 .meta("Today")
66 .paint(band(main, 0, 2), buf, theme);
67 let user = Message::new(
68 "You",
69 "Build a beautiful workspace. Keep the receipts legible.",
70 )
71 .role(Role::Primary);
72 let user_height = user.height(main.width, theme);
73 user.paint(band(main, 3, user_height), buf, theme);
74 let reply_top = 4 + user_height;
75 let reply = Message::new(
76 "Codewhale",
77 if regions.side.is_some() {
78 "The conversation stays at the center. Files, decisions and agent work sit alongside it.\n\nYou can open a result, inspect a command, or compose a denser workspace when the job needs it."
79 } else {
80 "The conversation stays at the center. Files and decisions are one action away.\n\nI am keeping the narrow layout readable."
81 },
82 );
83 let reply_height = reply.height(main.width, theme);
84 reply.paint(
85 band(
86 main,
87 reply_top,
88 reply_height.min(main.height.saturating_sub(reply_top + 6)),
89 ),
90 buf,
91 theme,
92 );
93 let tool_top = reply_top.saturating_add(reply_height + 1);
94 if main.height >= tool_top.saturating_add(12) {
95 ToolCard::new("Inspect", "workspace layout and rendered cells", State::Done).elapsed("12s").output("Wide: conversation + files\nNarrow: conversation, details on demand\nHost facts: preserved").paint(band(main, tool_top, 6), buf, theme);
96 let workbar = super::workbar::sample(crate::WorkbarPanel::Tasks);
97 let height = workbar
98 .height(main.width, theme)
99 .min(main.height.saturating_sub(tool_top + 12));
100 workbar.paint(band(main, tool_top + 7, height), buf, theme);
101 }
102 composer(main, buf, theme, "Show me the review, then keep going.");
103 if let Some(side) = regions.side {
104 PaneHeader::new("Files & review")
105 .meta("4 items")
106 .paint(band(side, 0, 2), buf, theme);
107 ArtifactShelf::new(files())
108 .selected(2)
109 .paint(band(side, 3, 9), buf, theme);
110 let attention = AttentionItem::new(
111 "Terminal",
112 "Review",
113 "Choose which preview to inspect first.",
114 State::NeedsYou,
115 )
116 .action("Open visual review")
117 .selected(true)
118 .focused(true);
119 attention.paint(band(side, 14, 7), buf, theme);
120 Whale::new(WhaleState::Think)
121 .words("Working alongside you")
122 .paint(band(side, 23, 11), buf, theme);
123 }
124 }
125
126 const PATCH: &str = "--- a/workspace.rs\n+++ b/workspace.rs\n@@ -18,3 +18,7 @@\n fn draw_workspace(area: Rect) {\n- draw_everything_in_one_column(area);\n+ let regions = frame.areas(area);\n+ conversation.paint(regions.main);\n+ if let Some(side) = regions.side {\n+ files.paint(side);\n+ }\n }\n";
127 fn review(area: Rect, buf: &mut Buffer, theme: &Theme) {
128 let area = area.intersection(buf.area);
129 let frame = WorkspaceFrame::new("codewhale-ratatui")
130 .branch("workspace-polish")
131 .mode("Review")
132 .footer("Review changes / Enter inspect / Esc close");
133 frame.paint(area, buf, theme);
134 let regions = frame.areas(area);
135 let main = regions.main;
136 PaneHeader::new("A workspace that adapts")
137 .meta("+5 / -1")
138 .paint(band(main, 0, 2), buf, theme);
139 Diff::new(parse_unified(PATCH))
140 .gutter(DiffGutter::Both)
141 .wrap(DiffWrap::Wrap)
142 .paint(band(main, 3, 12), buf, theme);
143 Heading::new("Run results")
144 .section()
145 .paint(band(main, 17, 2), buf, theme);
146 receipts().paint(band(main, 20, 7), buf, theme);
147 composer(
148 main,
149 buf,
150 theme,
151 "Keep the changes local until I inspect them.",
152 );
153 if let Some(side) = regions.side {
154 PaneHeader::new("A decision, in context").paint(band(side, 0, 2), buf, theme);
155 let subject = ApprovalSubject::new(ApprovalKind::Command, "git diff --check")
156 .cwd("codewhale-ratatui")
157 .scope(ApprovalScope::Inside)
158 .agent("Verifier", true)
159 .risk_note("checks whitespace in the current changes");
160 let state = ApprovalState::new(vec![
161 ApprovalChoice::new(ChoiceId(1), "Allow once", ApprovalEffect::Grants).char_key('y'),
162 ApprovalChoice::new(ChoiceId(2), "Deny", ApprovalEffect::Refuses).char_key('n'),
163 ])
164 .reveal(ApprovalKey::char('o'));
165 ApprovalCard::new(&subject, &state).paint(band(side, 3, 18), buf, theme);
166 let hints = KeyHints::new(vec![KeyHint::new("Enter", "inspect")]);
167 ReviewVerdict::new(
168 ReviewKind::Allowed,
169 "Layout review",
170 "The optional dock yields to conversation when space is limited.",
171 &hints,
172 None,
173 )
174 .expect("allowed fixture verdict")
175 .paint(band(side, 23, 9), buf, theme);
176 }
177 }
178 fn receipts() -> ReceiptTable {
179 ReceiptTable::new(
180 vec![
181 ReceiptColumn::new("Time").essential(),
182 ReceiptColumn::new("Tokens"),
183 ReceiptColumn::new("Cost").essential(),
184 ],
185 vec![
186 Receipt::new(State::Done, "Inspect files").values([
187 ReceiptValue::Duration(Some(Duration::from_secs(12))),
188 ReceiptValue::Tokens(Some(1240)),
189 ReceiptValue::Cost(Some(Cost::usd_cents(2))),
190 ]),
191 Receipt::new(State::Done, "Render previews").values([
192 ReceiptValue::Duration(Some(Duration::from_secs(4))),
193 ReceiptValue::Tokens(None),
194 ReceiptValue::Cost(None),
195 ]),
196 Receipt::new(State::Working, "Review narrow layout").values([
197 ReceiptValue::Duration(Some(Duration::from_secs(38))),
198 ReceiptValue::Tokens(Some(4800)),
199 ReceiptValue::Cost(Some(Cost::usd_cents(12))),
200 ]),
201 ],
202 )
203 .label_header("Reported work")
204 .totals()
205 }
206
207 fn fleet(area: Rect, buf: &mut Buffer, theme: &Theme) {
208 let area = area.intersection(buf.area);
209 let frame = WorkspaceFrame::new("codewhale-ratatui").branch("workspace-polish").mode("Station").side_width(0).footer("Illustrative / settled work keeps its receipt / host controls order, selection and actions");
210 frame.paint(area, buf, theme);
211 let main = frame.areas(area).main;
212 PaneHeader::new("One goal. Three agents. Every result visible.")
213 .meta("Station")
214 .paint(band(main, 0, 2), buf, theme);
215 Message::new(
216 "You",
217 "Build the workspace, review the interaction, and document the kit.",
218 )
219 .role(Role::Primary)
220 .paint(band(main, 3, 3), buf, theme);
221 let nodes = vec![
222 TreeNode::new(State::Working, "Finish the terminal workspace").children([
223 TreeNode::new(State::Done, "Builder / workspace composition")
224 .detail("Responsive regions, module headers, composer context")
225 .receipt("12 files / 2m 10s")
226 .opens(),
227 TreeNode::new(State::NeedsYou, "Reviewer / interaction and accessibility")
228 .detail("Choose the narrow preview to inspect")
229 .receipt("1 decision waiting")
230 .opens(),
231 TreeNode::new(State::Working, "Documentation / gallery and usage")
232 .detail("Actual cell buffers for every terminal profile")
233 .receipt("9 profiles / 38s")
234 .opens(),
235 ]),
236 ];
237 WorkflowTree::new(&nodes)
238 .summary()
239 .paint(band(main, 7, 7), buf, theme);
240 CountBar::new(3, 5)
241 .label("Reported tasks")
242 .paint(band(main, 15, 2), buf, theme);
243 if main.width >= 80 {
244 let left = Rect {
245 width: main.width / 2 - 2,
246 ..band(main, 20, 8)
247 };
248 receipts().paint(left, buf, theme);
249 let right = Rect::new(
250 main.x.saturating_add(main.width / 2 + 2),
251 main.y.saturating_add(20),
252 main.width.saturating_sub(main.width / 2 + 2),
253 8,
254 )
255 .intersection(main);
256 let items = [AttentionItem::new(
257 "Terminal",
258 "Reviewer",
259 "Inspect the 40-column composition before landing the change.",
260 State::NeedsYou,
261 )
262 .action("Open narrow preview")];
263 AttentionQueue::new(&items)
264 .selected(0)
265 .focused(true)
266 .paint(right, buf, theme);
267 }
268 super::workbar::sample(crate::WorkbarPanel::Fleet).paint(
269 band(main, main.height.saturating_sub(5), 5),
270 buf,
271 theme,
272 );
273 }
274 pub(crate) fn entries() -> Vec<Entry> {
275 vec![
276 Entry {
277 name: "workspace-scene",
278 width: 112,
279 height: 38,
280 draw: workspace,
281 },
282 Entry {
283 name: "review-scene",
284 width: 112,
285 height: 38,
286 draw: review,
287 },
288 Entry {
289 name: "fleet-scene",
290 width: 112,
291 height: 38,
292 draw: fleet,
293 },
294 Entry {
295 name: "workspace-scene-narrow",
296 width: 40,
297 height: 28,
298 draw: workspace,
299 },
300 ]
301 }
302
302 lines RUST