From 30df31b15a6bcf2d5ae3514877962ef786ecd52e Mon Sep 17 00:00:00 2001 From: Soham Marik Date: Sat, 25 Jul 2026 09:59:33 +0530 Subject: [PATCH 1/2] Add "Go to task" to the Today's 5 pinned-task options sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping a pinned task card opened a sheet with no way to navigate to the task itself — the only route was the card's inline open-in-new icon, which is easy to miss and sits right beside the remove (X) button. The sheet now offers the same explicit "Go to task" path the suggestion pill sheet does, placed above "Remove from Today's 5" to mirror that layout. The sheet body becomes scrollable: a fifth two-line option pushes the column past the default bottom-sheet height cap (9/16 of the screen), overflowing by ~47px on short screens such as a phone in landscape. Also: manual-test skill now requires icons be named the way the user sees them, not by their code identifier (open_in_new meant nothing to a tester looking at the screen). Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/manual-test/SKILL.md | 1 + docs/TEST_COVERAGE.md | 4 +- docs/UI_VIEWS.md | 2 +- lib/screens/todays_five_screen.dart | 24 ++++- test/screens/todays_five_screen_test.dart | 125 ++++++++++++++++++++++ 5 files changed, 150 insertions(+), 6 deletions(-) diff --git a/.claude/skills/manual-test/SKILL.md b/.claude/skills/manual-test/SKILL.md index 94aa82c..99b144e 100644 --- a/.claude/skills/manual-test/SKILL.md +++ b/.claude/skills/manual-test/SKILL.md @@ -36,6 +36,7 @@ Generate a checklist of manual tests the user should run for recent changes. - **Each section must be self-contained, with its OWN setup.** Tests within a section can build on each other, but a new section/subheading must never assume state from a previous section — including the shared "Setup" block, which only bootstraps the FIRST section. Every section that needs a precondition MUST carry its own setup steps (a per-section "Setup" sub-block, or fold the steps into the section) that create/guarantee that precondition from scratch. Walk each section as if the user jumped straight to it: if it needs a **free Today's 5 slot**, a **non-leaf task to drill into**, an **existing dependency**, an **unpinned leaf**, etc., spell out how to reach that state right there. Do not make the user hunt through their existing data for a task in the right shape, and do not make them undo leftover state from an earlier section (e.g. "you had to unpin an already-pinned task") — the section's own setup owns that. - **Every test case must name the screen/tab** where the action starts (e.g. "On Today's 5 tab, tap..." not just "Tap..."). Never assume the user knows which screen you mean from context. - **Name every control by its exact label/icon — never a pronoun.** Do not write "tap it", "select that", or "confirm it" — even when you named the control earlier in the same line. Re-state the literal label or icon the user must tap (e.g. "tap **'Remove from Today's 5'**", not "tap it"). A pronoun forces the user to guess which of several controls you mean. + - **Never name an icon by its code identifier alone — describe it the way the user sees it.** The tester is looking at the screen, not the source: `open_in_new`, `Icons.today`, `archiveIcon` mean nothing to them. Always lead with a **plain-language description of the icon's appearance and/or purpose**, and append the code name in parentheses only when it adds clarity for a control that's otherwise hard to pin down. Write "the **open-in-new-window icon** (a small box with an arrow leaving it)" or "the **'Go to task' arrow icon**" — NOT "the `open_in_new` icon". Same for any other identifier that only exists in code (widget class names, constants, callback names): if it isn't visible on screen, it can't be the primary way you point at something. - **State-dependent steps must guarantee the precondition, not assume it.** When a step needs a task to be *unpinned* (or pinned, leaf, empty, etc.), do not name a specific task that an earlier step or the user's existing data may have already moved out of that state. Either use a dedicated fixture created in Setup for exactly this state, or phrase it state-relative — e.g. "pick **any task not currently in Today's 5**" instead of "tap into `Bravo leaf`". Trace cumulative state through all prior steps before naming a task in a precondition. - **One test = one flow.** Each test case should verify a single behavior. Don't combine multiple if-else outcomes into one test (e.g. "should show X if Y, otherwise Z"). Split into separate tests with clear preconditions. - **Account for state consumed by prior tests.** If test 1 uses a task with a deadline and removes it, test 2 can't reuse that same task for a "keep deadline" test. Either specify separate tasks in the setup, or tell the user to undo/re-add state between tests. **This includes structural state:** if a test relies on a task being a *leaf* (childless), *non-leaf*, *pinned*, *empty*, etc., do NOT reuse a task that an earlier test mutates out of that state (e.g. a "leaf" test must not use a task that an earlier test added a child to). Use a **dedicated fixture task per structural precondition**, named so it's obvious (e.g. "Solo leaf task — never gets children"), and trace the cumulative state through every prior step before asserting a precondition. diff --git a/docs/TEST_COVERAGE.md b/docs/TEST_COVERAGE.md index 81f6f52..7b5f3fe 100644 --- a/docs/TEST_COVERAGE.md +++ b/docs/TEST_COVERAGE.md @@ -1,6 +1,6 @@ # Test Coverage Inventory -Last updated: 2026-07-18 +Last updated: 2026-07-25 ## Summary @@ -27,7 +27,7 @@ Note: `database_helper_test.dart` also covers **deadline auto-pin suppression** - **test/providers/auth_provider_test.dart** (6 tests) — `setSyncStatus` updates/notifications, `isConfigured`, initial state. ### Screens (partial) -- **test/screens/todays_five_screen_test.dart** (~64 tests) — Empty state, task rendering (max 5, leaf-only, blocked excluded), bottom sheet options, swap/navigate buttons, progress bar, priority icon, refresh dialog, DB state restore, SharedPrefs→DB migration, sync reload (task list replacement, completed status from remote), **deadline auto-pin override** (unpinned deadline stays unpinned on reload, generate doesn't re-pin suppressed tasks, suppression cleared on re-pin, re-pin persists across reload), **first generation auto-pin** (deadline task auto-pinned, `on` deadline auto-pinned on day, suppressed not pinned, max 5 respected, midnight rollover triggers auto-pin, reroll/New set does NOT auto-pin), **scheduled-today icon** (shown/hidden based on schedule, combined with deadline icon), **backfill params** (restore/refreshSnapshots/pinned-descendant backfill passes schedule+deadline+norm to `pickWeightedN`, lazy `_fetchSelectionContext` in `refreshSnapshots`, `_replaceIfNoLongerLeaf` on uncomplete), **reserved slots** (single scheduled source guaranteed slot, two sources each get a slot, ≥1 general-pool slot always preserved, cap at 4 reserved when 5+ sources), **manual model** (empty even when leaf tasks exist but no saved state — no auto-population), **implicit-pin remove** (Remove tile + card X both show a confirm dialog; Cancel keeps, Confirm removes from Today's 5 while task survives in DB), **Add to Today's 5 FAB sheet** (FAB opens sheet with "Create new task" / "Pick existing task"; "Pick existing" opens the browse-mode `TaskPickerDialog`; selecting a task pins it into Today's 5 and persists taskIds+pinnedIds; picker excludes tasks already in Today's 5), **+ FAB visibility at max pins** (FAB hidden once Today's 5 holds 5 tasks, shown below the limit), **midnight rollover** (debug night-icon rollover clears yesterday's lingering tasks to the empty "Nothing pinned yet" state — regression for the empty-load path not resetting `_todaysTasks`), **deadline-today auto-pin** (`feature/deadline-today-autopin` — manual-model exception that force-pins leaf tasks whose deadline is EXACTLY today into Today's 5 on every load): auto-pins a leaf due today with no saved state and persists it; overdue (yesterday) deadline NOT auto-pinned; suppressed task not re-pinned on reconcile; removing one deadline task still auto-pins a different one; removing an auto-pinned task records a suppression; **merges deadline auto-pin with an existing manual pin** (both appear — merge, not replace); **auto-pins every deadline task even beyond the maxPins cap** (the merge loop has NO cap — documents actual behavior, see finding below); **does NOT auto-pin a completed deadline-today task** (`getDeadlinePinLeafIds` excludes completed leaves); **re-pinning a suppressed deadline task clears its suppression** (exercises `_pinTaskInTodaysFive` → `unsuppressDeadlineAutoPin` via the Pick-existing flow); **suppression persists across a reload after removal** (remove via X → rebuild screen → still gone); **refreshSnapshots auto-pins a task that became due today** (deadline set to today while app open pins via the in-place refresh path, no full reload/restart); **refreshSnapshots does NOT re-pin a suppressed deadline task** (suppression respected on the refresh path too), **duplicate-task suggestion → Pin instead (create-new flow)** (`feature/duplicate-task-suggestion`: in the FAB → "Create new task" dialog, typing a name matching an existing task and tapping the "Pin instead" suggestion pins the EXISTING task into Today's 5 (`_pinTaskInTodaysFive`) with no duplicate; when the match is already in Today's 5, tapping it shows the "already in Today's 5" snackbar and does not re-add a duplicate slot; a same-named NON-leaf parent is NOT offered as a "Pin instead" suggestion — the match pool is leaves only (`getAllLeafTasks`), so typing a non-leaf's name surfaces no "Did you mean" panel). Note: first-gen/reroll auto-pin and the prior deadline auto-pin-override tests describe the older automated model and may no longer apply under the manual model. **Sectioned Today tab (`feature/todays-five-suggestions`)** — the Today body is now a constant-structure Column (flexible Today's-5 area on top + bottom-pinned "Suggested"/"Also done today"). **Opt-in Suggested section**: hidden behind "Show suggestions" by default; expanding surfaces weighted `pickWeightedN` picks; excludes tasks already in Today's 5; accepting pins into Today's 5; dismissing ("Not now") backfills a fresh pick; **unpinning after a full Today's 5 re-populates suggestions inline** (regression — the removal path (`_confirmRemoveFromTodaysFive`) now calls `_refreshSuggestions()`+`_loadTaskPaths()`+`setState`, so the re-shown section backfills at once instead of showing stale "No suggestions right now." until a tab switch, since `_suggestions` had been cleared by the `>= maxPins` guard while full); **a full Today's 5 (5 pins) hides the entire section** (baseline — the `showSuggestions = < maxPins` gate, complement to the unpin regression); **Hide collapses and clears the picks; re-expanding recomputes** (mechanism — `_toggleSuggestions` free-on-collapse/compute-on-expand); **a dismissed suggestion is suppressed for the session and does not reappear** (edge — lone candidate → dismiss exhausts the pool + records `_dismissedSuggestionIds`, section falls to empty text; distinct from the count-only backfill test); **a blocked leaf is not offered** (edge — `_refreshSuggestions` filters `getBlockedChildIds`). **Always-on "Also done today"**: surfaces even with nothing pinned (empty-state test), and **renders every done chip regardless of count** (regression — the fit-one-row `_chipsOverflow`/`_otherDoneExpanded` collapse was replaced by a single sideways-scrolling strip that builds every chip; expand/collapse chevrons gone). +- **test/screens/todays_five_screen_test.dart** (~68 tests) — Empty state, task rendering (max 5, leaf-only, blocked excluded), bottom sheet options, swap/navigate buttons, progress bar, priority icon, refresh dialog, DB state restore, SharedPrefs→DB migration, sync reload (task list replacement, completed status from remote), **deadline auto-pin override** (unpinned deadline stays unpinned on reload, generate doesn't re-pin suppressed tasks, suppression cleared on re-pin, re-pin persists across reload), **first generation auto-pin** (deadline task auto-pinned, `on` deadline auto-pinned on day, suppressed not pinned, max 5 respected, midnight rollover triggers auto-pin, reroll/New set does NOT auto-pin), **scheduled-today icon** (shown/hidden based on schedule, combined with deadline icon), **backfill params** (restore/refreshSnapshots/pinned-descendant backfill passes schedule+deadline+norm to `pickWeightedN`, lazy `_fetchSelectionContext` in `refreshSnapshots`, `_replaceIfNoLongerLeaf` on uncomplete), **reserved slots** (single scheduled source guaranteed slot, two sources each get a slot, ≥1 general-pool slot always preserved, cap at 4 reserved when 5+ sources), **manual model** (empty even when leaf tasks exist but no saved state — no auto-population), **implicit-pin remove** (Remove tile + card X both show a confirm dialog; Cancel keeps, Confirm removes from Today's 5 while task survives in DB), **Add to Today's 5 FAB sheet** (FAB opens sheet with "Create new task" / "Pick existing task"; "Pick existing" opens the browse-mode `TaskPickerDialog`; selecting a task pins it into Today's 5 and persists taskIds+pinnedIds; picker excludes tasks already in Today's 5), **+ FAB visibility at max pins** (FAB hidden once Today's 5 holds 5 tasks, shown below the limit), **midnight rollover** (debug night-icon rollover clears yesterday's lingering tasks to the empty "Nothing pinned yet" state — regression for the empty-load path not resetting `_todaysTasks`), **deadline-today auto-pin** (`feature/deadline-today-autopin` — manual-model exception that force-pins leaf tasks whose deadline is EXACTLY today into Today's 5 on every load): auto-pins a leaf due today with no saved state and persists it; overdue (yesterday) deadline NOT auto-pinned; suppressed task not re-pinned on reconcile; removing one deadline task still auto-pins a different one; removing an auto-pinned task records a suppression; **merges deadline auto-pin with an existing manual pin** (both appear — merge, not replace); **auto-pins every deadline task even beyond the maxPins cap** (the merge loop has NO cap — documents actual behavior, see finding below); **does NOT auto-pin a completed deadline-today task** (`getDeadlinePinLeafIds` excludes completed leaves); **re-pinning a suppressed deadline task clears its suppression** (exercises `_pinTaskInTodaysFive` → `unsuppressDeadlineAutoPin` via the Pick-existing flow); **suppression persists across a reload after removal** (remove via X → rebuild screen → still gone); **refreshSnapshots auto-pins a task that became due today** (deadline set to today while app open pins via the in-place refresh path, no full reload/restart); **refreshSnapshots does NOT re-pin a suppressed deadline task** (suppression respected on the refresh path too), **duplicate-task suggestion → Pin instead (create-new flow)** (`feature/duplicate-task-suggestion`: in the FAB → "Create new task" dialog, typing a name matching an existing task and tapping the "Pin instead" suggestion pins the EXISTING task into Today's 5 (`_pinTaskInTodaysFive`) with no duplicate; when the match is already in Today's 5, tapping it shows the "already in Today's 5" snackbar and does not re-add a duplicate slot; a same-named NON-leaf parent is NOT offered as a "Pin instead" suggestion — the match pool is leaves only (`getAllLeafTasks`), so typing a non-leaf's name surfaces no "Did you mean" panel). **Pinned task options sheet "Go to task" (`feature/pinned-task-navigate-option`)**: tapping a pinned card opens the options sheet, which shows "Go to task" (gated on `onNavigateToTask != null`) and tapping it navigates and closes the sheet; the tile is absent when `onNavigateToTask` is null (edge case — gating regression); all other options (Done today/Done for good!/In progress/Remove from Today's 5) still coexist alongside it (baseline — inserted, not swapped in); and on a short/narrow window (360x560) the now-`SingleChildScrollView`-wrapped sheet body lays out and scrolls to the last option ("Remove from Today's 5") with no `RenderFlex overflowed` exception (regression — the prior fixed-height `Padding`+`Column` overflowed by ~47px once the 5th two-line option was added). Note: first-gen/reroll auto-pin and the prior deadline auto-pin-override tests describe the older automated model and may no longer apply under the manual model. **Sectioned Today tab (`feature/todays-five-suggestions`)** — the Today body is now a constant-structure Column (flexible Today's-5 area on top + bottom-pinned "Suggested"/"Also done today"). **Opt-in Suggested section**: hidden behind "Show suggestions" by default; expanding surfaces weighted `pickWeightedN` picks; excludes tasks already in Today's 5; accepting pins into Today's 5; dismissing ("Not now") backfills a fresh pick; **unpinning after a full Today's 5 re-populates suggestions inline** (regression — the removal path (`_confirmRemoveFromTodaysFive`) now calls `_refreshSuggestions()`+`_loadTaskPaths()`+`setState`, so the re-shown section backfills at once instead of showing stale "No suggestions right now." until a tab switch, since `_suggestions` had been cleared by the `>= maxPins` guard while full); **a full Today's 5 (5 pins) hides the entire section** (baseline — the `showSuggestions = < maxPins` gate, complement to the unpin regression); **Hide collapses and clears the picks; re-expanding recomputes** (mechanism — `_toggleSuggestions` free-on-collapse/compute-on-expand); **a dismissed suggestion is suppressed for the session and does not reappear** (edge — lone candidate → dismiss exhausts the pool + records `_dismissedSuggestionIds`, section falls to empty text; distinct from the count-only backfill test); **a blocked leaf is not offered** (edge — `_refreshSuggestions` filters `getBlockedChildIds`). **Always-on "Also done today"**: surfaces even with nothing pinned (empty-state test), and **renders every done chip regardless of count** (regression — the fit-one-row `_chipsOverflow`/`_otherDoneExpanded` collapse was replaced by a single sideways-scrolling strip that builds every chip; expand/collapse chevrons gone). - **test/screens/completed_tasks_screen_test.dart** (~15 tests) — Empty state, completed/skipped display, today/older labels, restore/delete buttons, parent context, AppBar title. - **test/screens/starred_screen_test.dart** (44 tests) — Empty state, card display (single/multiple/subtitle/in-progress/tree preview/badge count), long-press navigation, drag handle, **tap expanded view** (dialog open, lazy-expand direct children on tap, collapse hides grandchildren, "No sub-tasks" leaf, star icon confirmation dialog, cancel keeps starred, confirm unstar + undo snackbar, undo re-stars, long-press tree node navigates, tap leaf navigates directly, dismiss by tapping outside, **child count badge**, **no chevron on leaves**, **header navigate icon**, **leaf navigate icon**), **unified tap** (leaf and non-leaf cards both open the expanded dialog, no navigate), **Add subtask FAB** (FAB opens AddTaskDialog, creates a subtask persisted under the starred card, "Add multiple" brain-dump creates several subtasks parented to the card — regression for "add multiple did nothing", cancel adds nothing, pinned starred task warns but does NOT transfer its pin to a child — manual model: the now-non-leaf parent drops out of Today's 5 instead, unpinned task shows no warning; **"Pin for today" toggle** — the subtask dialog shows a "Pin for today" chip (no Inbox chip); toggling it on pins the new subtask into Today's 5; the chip is **hidden** when the starred parent is itself pinned (drops out anyway) or when Today's 5 is already full; **cross-add refresh regression** — with 4 pinned fillers (one free slot) the toggle shows on the first add, and once pinning the new subtask fills Today's 5 to 5 the toggle is gone on the SECOND add within the same dialog, guarding `_reloadAfterAdd`→`_loadTodays5PinState` + the `onTodaysFiveChanged` count update against a stale `_todays5PinnedCount`). **screen-level Add task FAB** (FAB shown on the empty state, opens AddTaskDialog with the Inbox toggle; creates an auto-starred root task that appears in `getStarredTasks` with no parent). **starOrder preservation** (explicit starOrder on re-star, appends to end without), **grandchild DRY-rule styling (I-53)** (a high-priority grandchild is accent-tinted in the tree-preview card — not just the expanded dialog — via the shared `childTextStyle`; a blocked grandchild is dimmed (alpha 100) in the card, which required adding shown-grandchild ids to the `_blockedInfo` fetch that previously covered only direct children). **duplicate-task suggestion (`feature/duplicate-task-suggestion`)** — screen-level Add FAB "Star instead": tapping the suggestion stars the EXISTING task (`updateTaskStarred`) so it appears on the page with no duplicate, and an already-starred match shows the "already starred" snackbar as a no-op; expanded-dialog "Add subtask" FAB "Add here": tapping the suggestion links the existing task as a subtask (`addParentToTask`) with no duplicate, typing the starred parent's own name refuses to self-parent ("that's this task", no self-loop edge), and linking an ancestor of the starred parent surfaces the "would create a loop" warning and leaves the graph unchanged (distinct starred code path from the All Tasks cycle guard); the "Star instead" and "Add here" snackbars offer **Undo** (unstar / `removeParentFromTask`); and the **Codex P2 already-subtask guard** — an already-existing subtask short-circuits with an "already a subtask" message and NO destructive Undo, preserving the pre-existing edge. - **test/screens/starred_screen_reorder_test.dart** (12 tests) — `reorderByDependencyChains`: empty deps no-op, single blocker→dependent pair, blocked-before-blocker reorder, transitive chain (A→B→C), reverse-order chain, unrelated tasks preserved, multiple dependents per blocker, empty task list, single task, missing task IDs in deps, independent chains, deep 4-task chain. diff --git a/docs/UI_VIEWS.md b/docs/UI_VIEWS.md index 47e404b..e40a8ed 100644 --- a/docs/UI_VIEWS.md +++ b/docs/UI_VIEWS.md @@ -19,7 +19,7 @@ Reference for manual test instructions. Use these names consistently. - **Pick existing task** (`search`) — opens the **"Pin a task to Today's 5" dialog**, modeled on the triage dialog: always-visible search bar at the top, browse tree below. **Search mode** (when filter is non-empty) lists all leaf tasks matching the query (matches against task name and parent names). **Browse mode** (default) shows root-level tasks (excluding inbox tasks); tapping a non-leaf (`folder_outlined` icon, chevron trailing) drills into its children, tapping a leaf (`push_pin_outlined` icon, check trailing) pins it. A back arrow returns up the browse stack. Tasks already in Today's 5 are hidden from both views. When a search matches nothing, the empty state shows a **Create "<query>"** button — tapping it routes into the same **Create new task** flow above (Add Task dialog pre-filled with the query, created at root and auto-pinned into Today's 5). Exception: if the query **exactly matches a task already in Today's 5** (those are hidden from the search pool), the empty state instead reads **"<name> is already in Today's 5"** with **no Create button** — preventing an accidental duplicate. When Today's 5 is already full (5 tasks), the + FAB is hidden (see above), so neither option is reachable in the first place; the "Today's 5 is full — remove one first" snackbar is only a defensive guard for the rare case a slot fills (deadline-today auto-pin or a sync pull) after the sheet/dialog is already open. - **Task card** — each task in the Today's 5 list. Shows task name, subtitle icons (priority, deadline, scheduled today, started, someday). Done tasks are faded with strikethrough. Trailing icons depend on state: **undone** tasks show the **remove (X)** button + **"Go to task"** (`open_in_new`); **"Done today"** (worked-on, not completed) tasks keep **"Go to task"** but have no X; **"Done for good"** (completed) tasks show a **"View in archive"** archive icon instead of "Go to task" and have no X. No done task shows the X — uncomplete a "Done today" task by tapping it to take it off the list. - **Remove (X) button** — trailing icon on the right edge of the card (vertically centered, just before the "Go to task" `open_in_new` icon), undone tasks only. Tap shows the **"Remove from Today's 5?" confirmation dialog** (Cancel / Remove). On confirm, the task is dropped from Today's 5. The task itself is not deleted — it remains in All Tasks and can be re-added. -- **Bottom sheet** — appears when **tapping** an undone task card. Options: "Done today", "Done for good!", "In progress"/"Stop working", and "Remove from Today's 5" (which also goes through the same confirmation dialog). +- **Bottom sheet** — appears when **tapping** an undone task card. Options: "Done today", "Done for good!", "In progress"/"Stop working", **"Go to task"** (opens it in All Tasks — same action as the card's inline `open_in_new` icon), and "Remove from Today's 5" (which also goes through the same confirmation dialog). - **Completion animation** — brief celebration overlay after marking done. - **"Also done today" section** — a bordered box (label + a **single horizontal row of green ✓ chips that scrolls sideways**; one row tall, no expand/collapse) showing tasks worked on today outside the Today's 5 set. Shown **whenever something was done today**, independent of whether Today's 5 has any pinned tasks (so it appears in the empty state too, in the same position as when populated). When Today's 5 is populated, a "+N others" badge also appears on the progress line. - **Progress bar** — segmented progress bar at top showing done/total. diff --git a/lib/screens/todays_five_screen.dart b/lib/screens/todays_five_screen.dart index 5e7bee0..3443584 100644 --- a/lib/screens/todays_five_screen.dart +++ b/lib/screens/todays_five_screen.dart @@ -617,14 +617,19 @@ class TodaysFiveScreenState extends State await _persistAndTrim(); } - /// Shows a bottom sheet: "In progress" / "Done today" / "Done for good!" / - /// "Remove from Today's 5". + /// Shows a bottom sheet: "Done today" / "Done for good!" / "In progress" + /// (or "Stop working") / "Go to task" / "Remove from Today's 5". void _showTaskOptions(Task task) { final colorScheme = Theme.of(context).colorScheme; showModalBottomSheet( context: context, builder: (ctx) => SafeArea( - child: Padding( + // Scrollable: with 5 two-line options the column exceeds the default + // bottom-sheet height cap (9/16 of the screen) on short screens — a + // phone in landscape or a small device overflowed by ~47px once + // "Go to task" was added. Scrolling keeps every option reachable + // while still sizing to content when there's room. + child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, @@ -667,6 +672,19 @@ class TodaysFiveScreenState extends State _stopWorking(task); }, ), + // Same "Go to task" affordance the suggestion sheet offers — the + // card's inline open_in_new icon is easy to miss (and easy to + // mis-tap next to the X), so the sheet gets an explicit path too. + if (widget.onNavigateToTask != null) + ListTile( + leading: Icon(Icons.open_in_new, color: colorScheme.tertiary), + title: const Text('Go to task'), + subtitle: const Text('Open it in All Tasks'), + onTap: () { + Navigator.pop(ctx); + widget.onNavigateToTask!(task); + }, + ), ListTile( leading: Icon(Icons.close, color: colorScheme.onSurfaceVariant), title: const Text("Remove from Today’s 5"), diff --git a/test/screens/todays_five_screen_test.dart b/test/screens/todays_five_screen_test.dart index 2a5e3f6..42c9427 100644 --- a/test/screens/todays_five_screen_test.dart +++ b/test/screens/todays_five_screen_test.dart @@ -233,6 +233,131 @@ void main() { expect(navigatedTask!.name, 'Navigate me'); }); + testWidgets('pinned task options sheet has "Go to task" and it navigates', + (tester) async { + await tester.runAsync(() async { + final id = await db.insertTask(Task(name: 'Navigate me')); + await seedTodaysFive(db, [id]); + }); + Task? navigatedTask; + + await pumpAndLoad( + tester, + buildTestWidget(onNavigateToTask: (task) => navigatedTask = task), + ); + + // Tap the card body → options sheet. + await tester.tap(find.text('Navigate me')); + for (var i = 0; i < 10; i++) { + await tester.pump(const Duration(milliseconds: 50)); + } + expect(find.text('Go to task'), findsOneWidget); + + await tester.tap(find.text('Go to task')); + for (var i = 0; i < 10; i++) { + await tester.pump(const Duration(milliseconds: 50)); + } + expect(navigatedTask?.name, 'Navigate me'); + }); + + testWidgets( + 'options sheet has no "Go to task" tile when onNavigateToTask is null', + (tester) async { + await tester.runAsync(() async { + final id = await db.insertTask(Task(name: 'No nav')); + await seedTodaysFive(db, [id]); + }); + + // No onNavigateToTask callback supplied. + await pumpAndLoad(tester, buildTestWidget()); + + await tester.tap(find.text('No nav')); + for (var i = 0; i < 10; i++) { + await tester.pump(const Duration(milliseconds: 50)); + } + + // The other options are still present... + expect(find.text('Done today'), findsOneWidget); + expect(find.text('Done for good!'), findsOneWidget); + expect(find.text('In progress'), findsOneWidget); + expect(find.text("Remove from Today’s 5"), findsOneWidget); + // ...but the gated "Go to task" tile is absent. + expect(find.text('Go to task'), findsNothing); + }); + + testWidgets( + 'options sheet keeps every other option alongside "Go to task"', + (tester) async { + await tester.runAsync(() async { + final id = await db.insertTask(Task(name: 'All options')); + await seedTodaysFive(db, [id]); + }); + + await pumpAndLoad( + tester, + buildTestWidget(onNavigateToTask: (_) {}), + ); + + await tester.tap(find.text('All options')); + for (var i = 0; i < 10; i++) { + await tester.pump(const Duration(milliseconds: 50)); + } + + // All five options coexist — "Go to task" was inserted, not swapped in + // for an existing option. + expect(find.text('Done today'), findsOneWidget); + expect(find.text('Done for good!'), findsOneWidget); + expect(find.text('In progress'), findsOneWidget); + expect(find.text('Go to task'), findsOneWidget); + expect(find.text('Open it in All Tasks'), findsOneWidget); + expect(find.text("Remove from Today’s 5"), findsOneWidget); + }); + + // [Regression] Before the fix, the sheet body was a fixed-size + // Padding+Column: with all 5 two-line options present (including the new + // "Go to task" tile), the content exceeded the default bottom-sheet + // height cap (9/16 of the screen) on short screens, throwing a + // "RenderFlex overflowed by ~47px" layout exception. The body is now a + // SingleChildScrollView, so it must lay out (and be scrollable to the + // last option) without overflowing even on a short/narrow window. + testWidgets( + 'options sheet does not overflow on a short screen with all 5 options', + (tester) async { + // Mirror a small phone window where the previous fixed-height Column + // overflowed — the default 800x600 test surface has room to spare. + tester.view.physicalSize = const Size(360, 560); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.runAsync(() async { + final id = await db.insertTask(Task(name: 'Short screen task')); + await seedTodaysFive(db, [id]); + }); + + await pumpAndLoad( + tester, + buildTestWidget(onNavigateToTask: (_) {}), + ); + + await tester.tap(find.text('Short screen task')); + for (var i = 0; i < 10; i++) { + await tester.pump(const Duration(milliseconds: 50)); + } + + // No overflow exception was thrown during layout of the 5-option sheet. + expect(tester.takeException(), isNull); + + // The last option is reachable by scrolling the sheet body. + await tester.dragUntilVisible( + find.text("Remove from Today’s 5"), + find.byType(SingleChildScrollView), + const Offset(0, -50), + ); + expect(find.text("Remove from Today’s 5"), findsOneWidget); + expect(tester.takeException(), isNull); + }); + testWidgets('shows progress bar', (tester) async { await tester.runAsync(() async { final id = await db.insertTask(Task(name: 'Task 1')); From 0435f409f126a5325bc77317f540a8eb084e4f7f Mon Sep 17 00:00:00 2001 From: Soham Marik Date: Sat, 25 Jul 2026 10:07:33 +0530 Subject: [PATCH 2/2] Check pairing status before giving connect instructions in /debug-build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill jumped straight to `adb pair` instructions whenever no device was listed, but the phone is usually still paired from a previous session and only needs `adb connect` — the pairing flow is wasted steps. It now asks first whether this machine appears under "Paired devices" (bottom of the Wireless debugging screen, below both the QR-code and pairing-code options) and branches on the answer, using the real on-screen labels. The IP/port may be given as a bare port or a full ip:port — the port rotates each session while the IP usually persists, so a bare port paired with the remembered IP is the common case, not an error. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/debug-build/SKILL.md | 34 ++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/.claude/skills/debug-build/SKILL.md b/.claude/skills/debug-build/SKILL.md index eaebfe9..7bf2232 100644 --- a/.claude/skills/debug-build/SKILL.md +++ b/.claude/skills/debug-build/SKILL.md @@ -21,14 +21,32 @@ Build a debug APK and sideload it onto the connected Android device. Always recommend wireless debugging over USB when a device connection is needed. -To connect wirelessly: -1. On the phone: Settings → Developer Options → Wireless debugging → Enable it. -2. Tap "Pair device with pairing code" to get the IP, port, and pairing code. -3. Run `adb pair :` and enter the pairing code. -4. Then run `adb connect :` (the port shown on the Wireless debugging main screen, NOT the pairing port). -5. Verify with `adb devices`. - -If the device was previously paired, `adb connect :` alone should reconnect (no re-pairing needed), as long as both devices are on the same network. +**Ask about pairing status FIRST — don't assume either way.** Before giving any +connect instructions, have the user open **Settings → Developer Options → +Wireless debugging** on the phone. That screen shows, top to bottom: a +**"Use wireless debugging"** toggle, **"Device name"**, **"IP address and +port"** (e.g. `192.168.1.5:42173`), then **"Pair device with QR code"**, +**"Pair device with pairing code"**, and finally a **"Paired devices"** section +at the bottom listing this machine by its hostname (e.g. +`soham@soham-Dell-...`), with **"Currently connected"** underneath when a live +connection exists. + +Ask in ONE message for both: (a) whether this machine is listed under **"Paired +devices"**, and (b) the **"IP address and port"** — the user may answer with the +full `:` or just the port, whichever is easier; the port rotates every +session while the IP usually doesn't, so a bare port is a perfectly normal +answer. Pair it with the last known IP from memory, and only ask for the IP +explicitly if there's no saved one or the connect fails. Then branch: + +**Already paired** — no re-pairing needed: +1. Run `adb connect :` with the "IP address and port" value (the port changes every session; the IP usually doesn't). +2. Verify with `adb devices`. + +**Not paired** (or the connect above fails): +1. On the phone, tap **"Pair device with pairing code"** — it opens a dialog with its own IP, a *pairing* port, and a 6-digit code. +2. Run `adb pair :` and enter the pairing code. +3. Then run `adb connect :` using the **"IP address and port"** value from the main screen — NOT the pairing port (they differ). +4. Verify with `adb devices`. When no device is connected, check your memory for the last known IP. Try `adb connect :` with the user-provided port. If the connection fails, ask the user to verify the IP hasn't changed. When a connection succeeds, save the IP to memory for next time.