feat: Add keyboard shortcuts, export CSV, and last-updated indicator#1
feat: Add keyboard shortcuts, export CSV, and last-updated indicator#1
Conversation
- Add keyboard navigation (j/k, arrows, Enter to view, n to add) - Add keyboard shortcuts help modal (press ? to view) - Add CSV export functionality for mission items - Add last-updated timestamp with auto-refresh indicator - Visual highlight for keyboard-selected items
SummaryI built productivity enhancements for Mission Control that help power users navigate and manage items without reaching for the mouse. Changes MadeFile modified: New features:
Technical Details
Testing Checklist
Build Status✅ Build passes: |
📝 WalkthroughWalkthroughAdded client-side interactivity to the page component with keyboard navigation (j/k/Enter/n/Escape/?), CSV export functionality, auto-scrolling to selected items, item selection highlighting, and a keyboard shortcuts modal displaying available shortcuts. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Important Action Needed: IP Allowlist UpdateIf your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:
Failure to add the new IP will result in interrupted reviews. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4d5a633448
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| // Ignore if typing in an input | ||
| if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { | ||
| return; |
There was a problem hiding this comment.
Ignore keyboard shortcuts on interactive elements
The global keydown handler only exempts inputs and textareas, so shortcuts still fire when focus is on other interactive controls (e.g., the status <select>, buttons, or links). This prevents expected keyboard behavior like using Arrow keys inside the filter select or pressing Enter on the “Add Item” button, and instead moves the selection or opens a drawer. Consider skipping shortcuts when e.target is a select, button, a, or contentEditable, or when closest finds an interactive element.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@src/app/page.tsx`:
- Around line 234-238: The Escape key handler currently prevents default and
closes selected item/help but does not close the Add Item modal; update the
"Escape" case in the keyboard handler to also reset the add-modal state by
calling the modal setter (e.g., setShowAddModal(false)) alongside
setSelectedItem(null) and setKeyboardHelpOpen(false) so pressing Esc closes the
Add Item modal as well.
- Around line 203-208: The keyboard shortcut handler inside useEffect
(handleKeyDown) currently only ignores HTMLInputElement and HTMLTextAreaElement
and thus blocks normal interactions with <select> and contenteditable elements;
update the guard to also return early when e.target is an HTMLSelectElement or
when the event target (cast to HTMLElement) has isContentEditable === true (or
is inside a contenteditable) so arrow/Enter keys are not intercepted while
interacting with selects or contenteditable regions; modify the guard in
handleKeyDown to include these checks (use instanceof HTMLSelectElement and
(e.target as HTMLElement)?.isContentEditable or a closest('[contenteditable]')
check).
- Around line 509-511: Update the modal copy to match actual behavior: replace
the <p> element whose className is "mt-4 text-xs text-muted-foreground
text-center" (currently containing "Press any key to dismiss") with text that
accurately describes how to close the modal, e.g. "Press Esc to dismiss" (or
"Press Esc or click outside to dismiss" if you plan to add click-to-dismiss).
Ensure you only change the string content in the JSX where that <p> is rendered
(in the modal markup) so the UI copy matches the implemented Esc key handler.
- Around line 176-193: The CSV export in handleExportCSV currently quotes cells
but does not mitigate Excel formula injection; update the rows/CSV building so
any cell value that is a string and begins with one of the dangerous characters
(=, +, -, @) is prefixed (e.g., with a single quote) before being quoted.
Concretely, in handleExportCSV (where rows are created and csvContent is built)
add a small sanitizer that checks each cell (String(cell)) and, if it matches
the regex /^[=+\-@]/, prepends a safe prefix (such as "'") and then continues to
escape internal quotes and join — apply this sanitizer in the rows.map or right
before mapping row.map(...) so all exported cells are protected.
🧹 Nitpick comments (1)
src/app/page.tsx (1)
195-199: Revoke the object URL after download.
URL.createObjectURLis never revoked, which can leak memory on repeated exports.♻️ Suggested fix
- const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); + const link = document.createElement("a"); + const url = URL.createObjectURL(blob); + link.href = url; link.download = `mission-items-${new Date().toISOString().split("T")[0]}.csv`; link.click(); + setTimeout(() => URL.revokeObjectURL(url), 0);
| // Export to CSV | ||
| const handleExportCSV = () => { | ||
| const headers = ["Title", "Description", "Category", "Status", "Created", "Updated", "Notes", "Link"]; | ||
| const rows = filteredItems.map((item) => [ | ||
| item.title, | ||
| item.description || "", | ||
| item.category, | ||
| item.status, | ||
| item.created, | ||
| item.updated, | ||
| item.notes || "", | ||
| item.link || "", | ||
| ]); | ||
|
|
||
| const csvContent = [ | ||
| headers.join(","), | ||
| ...rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")), | ||
| ].join("\n"); |
There was a problem hiding this comment.
Mitigate CSV formula injection.
User-supplied fields can start with =, +, -, or @, which may execute formulas when opened in Excel/Sheets. Prefix such cells before quoting.
🛡️ Suggested fix
const handleExportCSV = () => {
+ const escapeCell = (cell: string) => {
+ const value = String(cell ?? "");
+ const safe = /^[=+\-@]/.test(value) ? `'${value}` : value;
+ return `"${safe.replace(/"/g, '""')}"`;
+ };
const headers = ["Title", "Description", "Category", "Status", "Created", "Updated", "Notes", "Link"];
const rows = filteredItems.map((item) => [
item.title,
item.description || "",
item.category,
item.status,
item.created,
item.updated,
item.notes || "",
item.link || "",
]);
const csvContent = [
headers.join(","),
- ...rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")),
+ ...rows.map((row) => row.map(escapeCell).join(",")),
].join("\n");📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Export to CSV | |
| const handleExportCSV = () => { | |
| const headers = ["Title", "Description", "Category", "Status", "Created", "Updated", "Notes", "Link"]; | |
| const rows = filteredItems.map((item) => [ | |
| item.title, | |
| item.description || "", | |
| item.category, | |
| item.status, | |
| item.created, | |
| item.updated, | |
| item.notes || "", | |
| item.link || "", | |
| ]); | |
| const csvContent = [ | |
| headers.join(","), | |
| ...rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")), | |
| ].join("\n"); | |
| // Export to CSV | |
| const handleExportCSV = () => { | |
| const escapeCell = (cell: string) => { | |
| const value = String(cell ?? ""); | |
| const safe = /^[=+\-@]/.test(value) ? `'${value}` : value; | |
| return `"${safe.replace(/"/g, '""')}"`; | |
| }; | |
| const headers = ["Title", "Description", "Category", "Status", "Created", "Updated", "Notes", "Link"]; | |
| const rows = filteredItems.map((item) => [ | |
| item.title, | |
| item.description || "", | |
| item.category, | |
| item.status, | |
| item.created, | |
| item.updated, | |
| item.notes || "", | |
| item.link || "", | |
| ]); | |
| const csvContent = [ | |
| headers.join(","), | |
| ...rows.map((row) => row.map(escapeCell).join(",")), | |
| ].join("\n"); |
🤖 Prompt for AI Agents
In `@src/app/page.tsx` around lines 176 - 193, The CSV export in handleExportCSV
currently quotes cells but does not mitigate Excel formula injection; update the
rows/CSV building so any cell value that is a string and begins with one of the
dangerous characters (=, +, -, @) is prefixed (e.g., with a single quote) before
being quoted. Concretely, in handleExportCSV (where rows are created and
csvContent is built) add a small sanitizer that checks each cell (String(cell))
and, if it matches the regex /^[=+\-@]/, prepends a safe prefix (such as "'")
and then continues to escape internal quotes and join — apply this sanitizer in
the rows.map or right before mapping row.map(...) so all exported cells are
protected.
| useEffect(() => { | ||
| const handleKeyDown = (e: KeyboardEvent) => { | ||
| // Ignore if typing in an input | ||
| if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Guard shortcuts for select/contenteditable (currently breaks form controls).
Arrow keys and Enter are intercepted even when focus is on a <select> or contenteditable element, which blocks normal form interaction. Expand the editable-target check.
🛠️ Suggested fix
- // Ignore if typing in an input
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
- return;
- }
+ // Ignore if typing/selecting in editable controls
+ const target = e.target as HTMLElement | null;
+ if (
+ target &&
+ (target.isContentEditable ||
+ target.closest("input, textarea, select, [contenteditable='true']"))
+ ) {
+ return;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| // Ignore if typing in an input | |
| if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { | |
| return; | |
| } | |
| useEffect(() => { | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| // Ignore if typing/selecting in editable controls | |
| const target = e.target as HTMLElement | null; | |
| if ( | |
| target && | |
| (target.isContentEditable || | |
| target.closest("input, textarea, select, [contenteditable='true']")) | |
| ) { | |
| return; | |
| } |
🤖 Prompt for AI Agents
In `@src/app/page.tsx` around lines 203 - 208, The keyboard shortcut handler
inside useEffect (handleKeyDown) currently only ignores HTMLInputElement and
HTMLTextAreaElement and thus blocks normal interactions with <select> and
contenteditable elements; update the guard to also return early when e.target is
an HTMLSelectElement or when the event target (cast to HTMLElement) has
isContentEditable === true (or is inside a contenteditable) so arrow/Enter keys
are not intercepted while interacting with selects or contenteditable regions;
modify the guard in handleKeyDown to include these checks (use instanceof
HTMLSelectElement and (e.target as HTMLElement)?.isContentEditable or a
closest('[contenteditable]') check).
| case "Escape": | ||
| e.preventDefault(); | ||
| setSelectedItem(null); | ||
| setKeyboardHelpOpen(false); | ||
| break; |
There was a problem hiding this comment.
Esc should also close the Add Item modal.
The PR objective states Esc closes modals/drawers, but showAddModal is not reset here.
🛠️ Suggested fix
case "Escape":
e.preventDefault();
setSelectedItem(null);
setKeyboardHelpOpen(false);
+ setShowAddModal(false);
break;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case "Escape": | |
| e.preventDefault(); | |
| setSelectedItem(null); | |
| setKeyboardHelpOpen(false); | |
| break; | |
| case "Escape": | |
| e.preventDefault(); | |
| setSelectedItem(null); | |
| setKeyboardHelpOpen(false); | |
| setShowAddModal(false); | |
| break; |
🤖 Prompt for AI Agents
In `@src/app/page.tsx` around lines 234 - 238, The Escape key handler currently
prevents default and closes selected item/help but does not close the Add Item
modal; update the "Escape" case in the keyboard handler to also reset the
add-modal state by calling the modal setter (e.g., setShowAddModal(false))
alongside setSelectedItem(null) and setKeyboardHelpOpen(false) so pressing Esc
closes the Add Item modal as well.
| <p className="mt-4 text-xs text-muted-foreground text-center"> | ||
| Press any key to dismiss | ||
| </p> |
There was a problem hiding this comment.
Modal copy doesn’t match behavior.
“Press any key to dismiss” isn’t implemented; only Esc closes it. Either update the copy or add an “any key” handler.
✏️ Suggested copy fix
- <p className="mt-4 text-xs text-muted-foreground text-center">
- Press any key to dismiss
- </p>
+ <p className="mt-4 text-xs text-muted-foreground text-center">
+ Press Esc to dismiss
+ </p>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <p className="mt-4 text-xs text-muted-foreground text-center"> | |
| Press any key to dismiss | |
| </p> | |
| <p className="mt-4 text-xs text-muted-foreground text-center"> | |
| Press Esc to dismiss | |
| </p> |
🤖 Prompt for AI Agents
In `@src/app/page.tsx` around lines 509 - 511, Update the modal copy to match
actual behavior: replace the <p> element whose className is "mt-4 text-xs
text-muted-foreground text-center" (currently containing "Press any key to
dismiss") with text that accurately describes how to close the modal, e.g.
"Press Esc to dismiss" (or "Press Esc or click outside to dismiss" if you plan
to add click-to-dismiss). Ensure you only change the string content in the JSX
where that <p> is rendered (in the modal markup) so the UI copy matches the
implemented Esc key handler.
What I Built
Added productivity enhancements to Mission Control to help power users navigate and manage items faster:
Features Added
Keyboard Navigation
j/ Arrow Down - Move to next itemk/ Arrow Up - Move to previous itemn- Open add new item modal?- Show keyboard shortcuts helpCSV Export
Last Updated Indicator
Visual Selection
How to Test
npm run devWhy It Matters
Built during Nightly Build Session (Feb 3, 2026)
Summary by CodeRabbit