| Age | Commit message (Collapse) | Author |
|
QuickAddActivity's autofocus used delay(150) then requestFocus() +
keyboard.show() -- a fixed delay racing against the window actually
gaining focus. This is a translucent, separate-taskAffinity popup
activity, so window focus transfer is variable/device-dependent; a
show() call made before the window is focused is silently dropped by
the IME service, no error, no retry -- the classic cause of "keyboard
doesn't appear until I tap out and back in" (reported 2026-08-06, "more
consistent here" than the same general Android quirk elsewhere). Fixed
to react to LocalWindowInfo.isWindowFocused instead of guessing a
timing window, plus windowSoftInputMode="adjustResize|stateVisible" as
an OS-level second line of defense.
Tried to verify live and hit a real limitation worth recording: this
box's AVDs run -no-window (headless), and in that mode mInputShown
never reports true via dumpsys input_method even for a manual,
deliberate tap on the field -- confirmed by testing a plain tap
directly, independent of any app code. The harness can't observe IME
visibility here, so this fix is verified by code-level reasoning
(LocalWindowInfo-driven focus is the standard, documented fix for this
exact bug class) and confirmed window-focus DOES transfers correctly
(mServedView moves to the bottom sheet's window), not by watching the
keyboard actually appear. Real confirmation has to happen on-device.
Also: SettingsActivity.saveAndFinish() only ever called
DootWidget().updateAll() indirectly, as a side effect of RefreshWorker
succeeding its network fetch -- so a slow or failing request could
delay or block the widget from reflecting a setting the user just
saved, even though every setting saved there (theme, text size,
background, checkboxes) is already fully local and needs no network
round trip to take effect. Now calls updateAll() directly and
immediately after writing prefs; RefreshWorker still runs afterward to
separately pull fresh server data.
Verified installable and crash-free on a real API 36 emulator.
Deployed as doot-widget.apk.
|
|
fixes, new settings
This lands the color-theming work that had sat uncommitted since a prior
session (2026-07-28) -- every build published in between stripped it out
deliberately to avoid shipping unreviewed work -- plus a full round of
fixes and new features layered on top since it finally shipped:
Theming (WidgetPalette.kt, new):
- 5 themes now: NEUTRAL/ACCENT/TONAL (wallpaper-derived via Material You),
VIVID (new -- all three text roles pull from a different accent slot
instead of anchoring primary to neutral, for real hue variety), CLASSIC
(fixed, wallpaper-independent).
- Settings picker redesigned to match Android's native wallpaper "Basic
colors" circular swatches (bottom half + two top quadrants, filled with
each theme's actual buildWidgetPalette() output, not an approximation).
- Per-source accent colors (colored checkboxes/bars) fully removed from
the grid.
Past events (DootWidget.kt, WidgetRows.kt):
- Already-ended-today events pulled out of the hourly grid, shown as list
rows above it instead (matching how past tasks already float) -- fixes
the grid's start hour getting stretched backward by stale events.
Legibility (WidgetRows.kt):
- ShadowedText upgraded from a single-corner drop shadow to a 4-corner
halo/outline (protects all sides of a glyph, not just one).
- Halo color now tracks each palette's text luminance (dark halo for
light text, light halo for dark text) -- a hardcoded black halo behind
already-dark light-mode text was doing essentially nothing. Bumped
opacity 0.45/0.55 -> 0.65/0.7 as the cheap, low-risk strength dial.
New settings (SettingsActivity.kt, DataStore.kt):
- Background transparency slider (0-85%, default 0% unchanged) --
exposed for testing per explicit request, not a default change.
- Hide-checkboxes toggle: drops the leading checkbox/dot element
entirely (not just hides the icon) so task titles land flush with
event titles; tap-to-complete-from-widget trades off for
TaskDetailActivity's Complete button.
Also: today's moon phase in the TODAY header (moonPhaseEmoji, pure
date computation, no network) -- verified against direct calculation
before writing test assertions, not hand-computed. Removed the unused
glance-material3 dependency (verified zero usages before removing;
turned out to save ~2KB, not the ~280KB expected, since material3
itself already pulls the same transitive deps -- noted honestly rather
than oversold).
Every new pure-logic piece has unit tests (isPastEvent, moonPhaseEmoji,
theme construction) -- 59 total, all green. Verified installable and
crash-free via a real API 36 emulator launch before each publish, not
assumed. Deployed as doot-widget.apk.
|
|
First cut per the 2026-08-06 feasibility check (verdict: easy, no
architecture blockers): a plain WebView pointed at the configured
server URL, cookie jar enabled for the existing session-cookie login
(internal/auth/middleware.go's RequireAuth) -- no separate auth bridge
needed, the widget's own bearer token is a completely different scheme
and doesn't need to touch this at all. External links (e.g. a calendar
event's source URL) escape to the user's real browser instead of
getting stuck in the WebView.
No deep-linking to specific tabs, no native-rendered chrome -- this is
the minimal first step to validate the wrapped experience is worth
building further, not the final shape.
|
|
Alongside Complete/Edit, doot-native tasks now get a Postpone button with
a dropdown (Tomorrow / Next week / Next month), reusing the existing
reschedule wiring (WidgetRepository.reschedule) the due-date picker
already uses.
Caught and documented a real divergence while writing the test: Java's
LocalDate.plusMonths CLAMPS to the target month's last valid day (Jan 31
-> Feb 28), while the Go server's ComputeNextOccurrence (recurrence
math) OVERFLOWS instead (Jan 31 + 1 month -> Mar 3) via time.AddDate.
Verified by actually running both, not assumed -- a first draft of this
test asserted the wrong (Go-style) behavior before checking. Not
reconciled here, just accurately documented as a known inconsistency
between this client-side helper and the server's date math.
4 new tests, all passing.
|
|
completions
Root cause traced from server logs, not guessed: every completion request
was succeeding server-side (100% 200s, including a 5-tap burst spanning
different tasks), and each successful completion's response payload was
correctly shrinking. So the failure wasn't dispatch or network -- it was
that a successful completion could still get silently undone client-side.
fetchAndPersist does an unconditional full overwrite of the cached item
list on every successful GET. CompleteWorker/DeferWorker run one instance
per task id with no ordering guarantee between different ids' workers
(different unique work names, no KEEP protection across them -- that
protection only ever covered same-task double-taps). So: tapping complete
on task A starts a GET that's still in flight; tapping complete on task B
before A's GET returns optimistically removes B locally; A's slower GET
response, captured before B's completion landed, then overwrites the
cache and silently resurrects B.
Fix: track locally-optimistic removals with a timestamp (PendingRemovals.kt)
and filter them out of every fetchAndPersist write for a bounded TTL (2
min), regardless of which worker's fetch is doing the writing. The TTL
means a completion that never actually confirms (permanent network
failure) still self-heals via the next periodic refresh, matching an
existing self-healing property already relied on elsewhere in this
codebase, instead of hiding the task forever.
Added PendingRemovalsTest.kt (pure-function unit tests, no Android
runtime needed) covering the exact race scenario plus TTL expiry and
edge cases. Verified the tests actually catch a regression by deliberately
reverting the fix to a no-op against a real backup, confirming 3 tests
failed with the exact expected assertion, then restoring and confirming
green again.
Built, tested, and published as doot-widget.apk.
|
|
addTask()'s failure path had no .onFailure handler, so a failed POST
(network hiccup, server down) left the Add sheet just sitting there
with zero feedback -- looked like the button did nothing.
Also: the five calls chained after a successful addTask (updateTask,
reschedule, setTaskProject, setTaskLabels, setTaskRecurrence) were
still unchecked, so a task could get created with silently-dropped
metadata even with the addTask fix in place. Both paths now collect
and toast what failed; the task itself still gets created and the
sheet still closes, since aborting would leave a task that already
exists server-side with no way to tell the user without a duplicate.
|
|
TomorrowSection emitted its divider, header, and every event/task row
as top-level siblings with no shared layout container. Since the whole
function is composed inside a single LazyColumn item{} slot (one
RemoteViews node), nothing told Glance to stack them vertically -- they
all rendered on top of each other instead of flowing top-to-bottom.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|
|
Backend, web timeline, and Android widget wiring for the last two
unimplemented items from doot-future-task-scheduling-ideas.
Chains: task_chains table + chain_id/chain_position/chain_unlocked on
native_tasks (migration 026), WIP-limit-1 advancement hooked into
CompleteNativeTask, locked tasks excluded from all date-based queries,
5 new /api/widget/chains* endpoints, an N/M position badge on web and
Android widget rows.
Buckets: maintenance_buckets table + bucket_id/bucket_state/
bucket_last_active_at on native_tasks (migration 027),
staleness-then-priority selection scoring, a new RunBucketCycleCheck
scheduler loop, 5 new endpoints including the distinct Defer action, a
Defer button on web and Android widget rows.
Also corrected stale "not yet approved" status headers on the two
already-shipped specs this work depended on (labels/projects, budgets/
availability) -- their headers were never updated after implementation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|
|
Quick Add's text field never requested focus or triggered the keyboard,
so tapping it opened a sheet with no visible way to type. Added a
FocusRequester + delayed requestFocus()/keyboard show (ModalBottomSheet
needs a beat to finish its enter animation before focus requests land).
While in there, rebuilt the quick-add sheet to match the task edit
popup instead of being a bare title field: due date, recurrence,
project, and label chips, plus a description field, reusing the same
dialogs the edit popup already uses. POST /api/widget/add now returns
the created task's id so the client can chain the same
project/labels/recurrence/due-date setter calls edit already relies on.
|
|
Threads the /api/widget budget_status field through the Android widget's
DataStore-backed cache (a new BUDGET_STATUS_JSON pref, since the widget
decomposes WidgetResponse into individual prefs rather than caching it
whole) and renders a small "Nm/Nm" badge next to TODAY when there's a
nonzero tracked load.
|
|
|
|
|
|
|
|
DootWidget.kt (582 lines) mixed the GlanceAppWidget entry point/root
composable, all the row-rendering composables, and multi-day-event
detection logic in one file. Split into:
- MultiDayEvents.kt: MultiDayVariant, effectiveEndDay, isMultiDayEvent,
multiDayVariant, multiDayLabel, timeSuffix.
- WidgetRows.kt: sourceColor, calendarViewIntent, and all row/section
composables (AllDayRow, RefreshButton, QuickAddButton, HourRow,
EventBlock, TaskFragmentBlock, TaskRow, TomorrowSection,
TomorrowEventRow), plus hourLabel/calcGridStart/calcGridEnd.
- DootWidget.kt: just the GlanceAppWidget class and WidgetRoot, now
145 lines.
All same package (org.terst.doot.widget.ui), so no import changes
needed for cross-file references. Pure move, no behavior change --
full unit test suite (including DootWidgetGridTest/
DootWidgetMultiDayTest, which call the internal functions directly)
passes unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
The 4 frequency chips and 7 weekday chips were each laid out in a
single non-wrapping Row -- on a real phone width these overflow/get
cut off rather than wrapping. The interval field was a full-width
OutlinedTextField with the unit baked into its label ("Every N
dailys"/"weeklys" -- ungrammatical for anything but weekly), and the
whole dialog had no visual grouping.
Redesigned: FlowRow (wraps instead of overflowing) for both chip rows;
single-letter weekday chips (S M T W T F S) to stay compact; a narrow
fixed-width (72dp) interval field paired with a correctly-pluralized
unit label rendered separately from the field; muted section labels
(REPEATS / EVERY / ON THESE DAYS) for structure. No callback/API
changes -- purely a layout rewrite of the same onSave/onClear/onDismiss
contract.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
TaskRow used a flat hardcoded gray (0xFFDDDDDD) while EventBlock used
Color.White (dimmed only when past) at the same size/weight -- the
color mismatch read as a font difference. Both now use the same
Color.White base.
CompleteTaskAction previously did no local update at all: the row
stayed visible until CompleteWorker's full complete() ->
fetchAndPersist() -> updateAll() round trip finished (two sequential
network calls). It now optimistically removes the completed item from
the cached list and re-renders immediately, matching RefreshTaskAction's
existing synchronous-flag-then-updateAll pattern -- the background
worker's own fetchAndPersist still replaces this with the authoritative
server state (including a newly-created recurring successor, if any)
moments later.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
onEnabled only fires when the first widget instance is added to a home
screen, so if the periodic RefreshWorker job is ever silently dropped
(an app reinstall can clear WorkManager's persisted schedule without
the widget itself being removed/re-added -- exactly what happens when
sideloading a new APK build, as opposed to a Play Store update), there
was no way for it to come back except manually removing and re-adding
the widget. onUpdate fires far more often (reboot, periodic OS ticks)
and now also re-arms the schedule (a no-op via KEEP if already running).
Root-caused via manual refresh restoring all missing content instantly
(rules out a data/rendering bug) plus the widget only showing one
stale morning event beforehand (consistent with the periodic job
having stopped ticking hours earlier, right around today's APK
reinstalls).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Overdue and non-overdue tasks now render with the same plain title
color; no more red highlighting for overdue items.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
Editable title/description (Edit -> Cancel/Save toggle), linkified
description, and independently-tappable date/recurrence/next-date
chips. Non-doot sources (Trello, Google Tasks) are unaffected --
same title + Complete button as before, no live fetch, no edit UI.
|
|
|
|
|
|
legibility, soften overdue red
- AllDayRow now has the same 32dp leading gutter as HourRow's hour-label
column, so its color bar lines up with EventBlock's bar in the grid
below instead of sitting flush left; also dropped the title's
defaultWeight() so the multi-day label sits right after the title
instead of being pushed to the far-right edge.
- TaskFragmentBlock no longer renders its own "TODAY" sub-header --
it's always inside the already-labeled TODAY section, and Tomorrow's
floating tasks never had this redundant label to begin with.
- Section headers and hour/time labels bumped from ~30-40% to ~50-60%
opacity -- against a home-screen wallpaper the previous values were
barely legible.
- Overdue task text alpha reduced to 0.75 -- full-opacity bright red
read as too alarming.
|
|
|
|
The label was concatenated into the title string, inheriting its 0.9
alpha. Split into its own Text at full opacity, matching EventBlock's
treatment of upcoming (non-past) events.
|
|
Multi-day events (Start and End on different calendar days) are pulled
out of the normal grid/all-day pipeline and rendered as an all-day-style
row on every day they touch (Today and/or Tomorrow), labeled
starts/ends/plain per the day being rendered. Previously such an event
either only appeared in the single hourly grid slot matching its start
time (never again on later days) or, if genuinely flagged all-day,
never had its End forwarded at all.
|
|
Tapping a calendar event (or any event-type item) now opens the event's
URL (Google Calendar, Plan to Eat, etc.) directly instead of showing an
intermediate popup with an "Open in Calendar" button. Removes
EventDetailActivity and the recurrence-schedule lookup it was the only
consumer of: WidgetRepository.getRecurrence, the Go
/api/widget/recurrence endpoint, HandleWidgetRecurrence,
GoogleCalendarAPI.GetRecurrenceRule, and formatRecurrence, plus their
tests. RecurringEventID itself stays -- it's general calendar-sync
metadata used elsewhere in the timeline pipeline, not exclusive to the
removed popup.
|
|
These were the only labels in the settings screen without an explicit
color, unlike the rest of the screen's text elements.
|
|
Headers (hour labels, section titles) shrinking along with content at
the SMALL setting made them too small relative to content. Content
still shrinks at SMALL; headers now stay at NORMAL's scale/weight
regardless of the selected tier.
|
|
Unused after threading WidgetTextSize's scaling functions through every
Text() call site that previously used a literal .sp value.
|
|
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
|
|
|
|
|
|
calcGridStart/calcGridEnd only looked at hour-of-day, so a tomorrow
event's early or late hour could inflate today's grid range with empty
rows, pushing the non-scrolling TomorrowSection below the widget's
visible area. Filter to today-only events before computing bounds.
|
|
Another session pushed 27 commits in parallel covering quick-add, event
detail popups, recurrence display, overdue badges, a manual refresh button,
and its own fix for the same overdue-tasks bug (via a separate
GetOverdueNativeTasks fetch folded into BuildTimeline, rather than widening
GetNativeTasksByDateRange's bound directly). Reconciled rather than blindly
taking one side:
- Reverted GetNativeTasksByDateRange to its original bounded query and kept
upstream's GetOverdueNativeTasks + BuildTimeline fold-in as the sole
overdue mechanism for native tasks, to avoid double-counting overdue
items (my widened query + their separate fetch would have both returned
them). Re-pointed the regression test at the now-correct contract and
added a store-level test for GetOverdueNativeTasks directly.
- Kept my GetGoogleTasksByDateRange fix as-is (single unbounded query) --
upstream never touched Google Tasks overdue handling, so there's no
duplication risk there.
- Rewove WidgetRoot's LazyColumn structure (added for scrolling) around
upstream's new header buttons, pinned all-day event rows, and the
enhanced TomorrowSection, none of which were written LazyColumn-aware
since that work landed on this side only.
- Combined both sides' additions to TaskDetailActivity/TaskDetailSheet
(description-edit detail popup + due-date reschedule label) and
WidgetRepository/Actions (optimistic local removal + refresh button
wiring) -- these were independent, non-overlapping features that both
needed to survive.
- Renumbered the migration collision: both sides independently added a
migration numbered 022. Card-description was already applied to the live
production DB under that filename earlier this session (migrations are
tracked by filename), so it keeps 022; the recurring-event-id migration,
never deployed under any name here, moves to 023.
Verified: go build clean, full test suite passes (only the two
pre-existing agent-handler failures and the pre-existing models package
build error remain, both confirmed unrelated via git stash before this
session began), and a dry run against a copy of the live production
database applies both migrations cleanly with no re-run conflicts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|
|
- Add description editing to the widget's task detail popup for
doot/gtasks/trello, backed by new GET /api/widget/detail and
POST /api/widget/update endpoints
- Make Google Tasks and Trello cards completable via the widget (Trello
completion archives the card); fix Trello description never being
fetched, which meant saving could silently wipe a card's real desc
- Fix google_tasks.due_date/updated_at (TEXT columns) never round-tripping
through sql.NullTime, which broke cached Google Tasks reads whenever
the cache was valid
- Fix native-task and Google-Task date-range queries excluding anything
due before the window start, which dropped incomplete tasks off the
widget the moment their due day passed (the "overdue tasks disappeared"
bug)
- Fix native task description edits blanking the task's title
- Make the widget's day list scroll (LazyColumn) instead of clipping
- Optimistically remove a task from the widget immediately on completion,
ahead of the authoritative background refresh
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
|
|
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
|
|
|
|
|
|
|
|
|
|
WidgetItem.isAllDay was carried by the client's data model but nothing
ever read it. All-day events had no Start at all, so they fell into the
same floating-task queue as ordinary untimed tasks; if enough tasks were
ahead of one in the queue, SlotPacker could assign it an hour slot past
the visible grid range entirely -- not merely unpinned, actually
invisible.
Server: TimelineItemToWidgetItem now populates Start for all-day
CALENDAR EVENTS specifically (their real event date), while leaving
undated doot/gtask tasks -- also flagged IsAllDay as a "no specific
time" fallback, a different concept -- on the existing nil-Start
floating behavior.
Client: all-day events are filtered out of the hourly grid/floating-task
pipeline entirely, bucketed by day using the new Start date, and
rendered in a new pinned AllDayRow section right after the TODAY/TOMORROW
headers.
|
|
CompleteWorker.enqueue used a plain WorkManager.enqueue(), which allows
unlimited concurrent OneTimeWorkRequests. A rapid double-tap on the same
row (plausible since the checkbox doesn't visually update until the full
async round-trip -- complete() -> fetchAndPersist() -> updateAll() --
finishes) could spawn two independent, unordered CompleteWorker runs for
the same task, each doing its own fetchAndPersist(); a second worker's
fetch started before the first worker's complete() call had actually
landed server-side could persist a stale snapshot after the first
worker's correct one.
Now uses enqueueUniqueWork("complete_$id", KEEP, ...) so a tap on a task
that already has a completion in flight is dropped rather than racing a
second worker. Different task ids remain independent.
|
|
In Glance 1.1.0 (RemoteViews), a parent Row with .clickable() silently
overrides any nested child .clickable() — tapping the checkbox fired the
detail-open action instead of CompleteTaskAction, producing no visible
effect. Fix by splitting TaskRow into two sibling Boxes: a 24dp tap
target wrapping the checkbox icon (routes to CompleteTaskAction) and a
defaultWeight Box for the title (routes to actionStartActivity). No
nesting, so both actions are independently reachable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Widget now renders a TOMORROW block below the today grid: events with
inline time labels (slightly dimmed) and task rows. Separated by a
divider. Covers both explicit-start tomorrow items and slot-packed
fragments that overflow from today.
Web view: TODO comment to flatten the tomorrow section to match widget.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Tapping a doot task shows a date picker. On confirm, POSTs to
/api/widget/reschedule, updates due_date in native_tasks, refreshes
widget. Reschedule button only shows for source="doot" tasks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Adds doot-owned task storage (native_tasks table) so tasks can be
managed without Todoist. CompleteTask for 'doot' source just updates
the DB — no external API call, no token dependency.
Migration path:
POST /settings/import-from-todoist — copies Todoist cache → native_tasks
Then remove TODOIST_TOKEN from .env to disable Todoist
Changes:
- migration 020: native_tasks table
- store: GetNativeTasks, GetNativeTasksByDateRange, GetUndatedNativeTasks,
CreateNativeTask, CompleteNativeTask, UncompleteNativeTask,
UpdateNativeTask, ImportFromTodoist
- timeline: native tasks appear as source="doot" (teal)
- handleAtomToggle: "doot" case — no external API needed
- HandleWidgetComplete: method on Handler, handles "doot" natively
- HandleUnifiedAdd: "doot" source creates in native_tasks
- widget: "doot" tasks are completable, teal color indicator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Replaces browser deep-link with a transparent TaskDetailActivity that
shows a Material3 ModalBottomSheet (20-40% screen height). The launcher
shows through the transparent window behind the dark scrim. Sheet shows
source color dot, task title, and Mark Complete button for Todoist tasks.
Tapping outside or swiping down dismisses. No browser involved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Adds GET /task?id=xxx&source=xxx route that renders a full mobile-friendly
task detail page (session-protected). Widget task rows now open this page
when tapped. HandleUpdateTask redirects back to the page after a non-HTMX
save. Android: threads serverUrl through composable chain to TaskRow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|