Ubisam-MobileGit/docs/development-log.md
choiminyeong 471456258b TASK-042: colored status badges for issue/PR state (열림/닫힘/병합됨)
Replaces the plain-text 열림/닫힘/병합됨 label on issue/PR list rows and
detail headers with a compact colored badge, reusing the VisibilityBadge
pattern TASK-039 established for 공개/비공개. OPEN reuses the app's green
primaryContainer, CLOSED reuses the baseline errorContainer, MERGED reuses
the teal tertiaryContainer -- all existing theme roles, so dark mode is
handled automatically with no new hardcoded colors.

Found while verifying issue/PR body rendering for real content (confirmed
correct, no bug) during the design-review pass. Live-verified on-device in
both light and dark mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 22:40:33 +09:00

890 lines
174 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UBGitea development log
This is the append-only implementation and handoff record shared by Codex and Claude. Only the current task owner or assigned reviewer may append an entry, and agents never edit this file concurrently.
## 2026-08-25 — TASK-001
- Agent and role: Codex, implementer and integrator
- Commit/diff: `75aa5f8`
- Features implemented: Android Compose project bootstrap, Korean placeholder home screen, deterministic Gradle wrapper, unit-test and lint baseline.
- How it was implemented: One application module using Kotlin, Compose, Material 3, API 28 minimum, API 36 target, JDK 17, and version-catalog-managed dependencies.
- Files changed: Root Gradle configuration, `app/`, ADR-001, product and task documentation.
- Checks and results: Debug APK build passed; 1 unit test passed; lint passed; sensitive-pattern scan found no assigned secret.
- Review findings and resolutions: Independent review found no P0-P3 findings.
- Known limitations: No device/emulator launch; final icon and authentication were out of scope.
- Next recommended task: Repository browsing foundations.
- Next owner: Codex.
## 2026-08-25 — TASK-003
- Agent and role: Codex, implementer and integrator
- Commit/diff: `a2cc7d4`
- Features implemented: Immutable repository, branch, tree, file, commit and paging domain contracts; safe coordinate/ref/path validation; read-only repository interface and browse use cases.
- How it was implemented: Pure Kotlin domain models, defensive byte/list copies, shared pagination validation, Korean read-only placeholder state, and fake-adapter unit tests.
- Files changed: Domain model/repository/use-case packages, focused tests, home placeholder, product roadmap and TASK-003 through TASK-007 documents.
- Checks and results: Build passed; 22 unit tests passed; lint passed; sensitive-pattern and diff checks passed.
- Review findings and resolutions: Two P2 findings covering page output mutability and missing use-case boundary tests were fixed and independently re-reviewed; no findings remain.
- Known limitations: Live HTTP, authentication, navigation, content rendering, and device testing were out of scope.
- Next recommended task: OAuth protocol core followed by Android OAuth integration.
- Next owner: Codex.
## 2026-08-25 — TASK-002A
- Agent and role: Codex, implementer and integrator
- Commit/diff: `6da016c`
- Features implemented: OAuth client configuration, approved read-only scopes, fresh PKCE S256 verifier/state generation, authorization URL construction, exact redirect validation, transaction expiry, denial/success modeling, and strict callback query validation.
- How it was implemented: Pure Kotlin/JDK code with `SecureRandom`, SHA-256, unpadded Base64 URL encoding, RFC 7636 verifier grammar, deterministic query component encoding, constant-time state comparison, strict UTF-8 percent decoding, duplicate/ambiguous response rejection, saturating time boundaries, and redacted string representations for sensitive values.
- Files changed: `core/auth/protocol/`, matching protocol tests, ADR-002, TASK-002 umbrella, TASK-002A, and TASK-002B.
- Checks and results: Focused OAuth tests 21/21 passed; full unit tests 43/43 passed; debug APK build passed; Android lint passed; diff check passed; sensitive-pattern scan found no assigned secret/token/password/private key.
- Review findings and resolutions: P2 verifier grammar fixed with strict RFC character/length validation; P2 parameter pollution and malformed UTF-8 fixed with fail-closed parsing; P3 timestamp overflow fixed with saturating boundaries. Independent re-review found no remaining P0-P3 findings.
- Known limitations: System browser launch, App Link dispatch, code exchange, Keystore token vault, user identity, and real-server E2E are intentionally deferred to TASK-002B. E2E remains blocked by signing fingerprints and hosted `assetlinks.json`.
- Next recommended task: TASK-002B Android OAuth integration, completing locally testable browser/storage/exchange boundaries before the external E2E gate.
- Next owner: Codex.
## 2026-08-25 — TASK-011
- Agent and role: Codex, implementer and integrator
- Commit/diff: current TASK-011 working-tree diff; user-owned `assetlinks.json` excluded
- Features implemented: System-browser Gitea OAuth login, verified HTTPS App Link callback, PKCE code exchange without a client secret, Keystore-backed token storage and logout, authenticated identity check, and a paginated list of every repository visible to the account.
- How it was implemented: Exact callback validation reuses TASK-002A; pending verifier/state is consumed once; OkHttp runs asynchronously with cancellation/error propagation; Gson DTOs fail closed into typed API errors; `/api/v1/user` precedes bounded/deduplicated `/api/v1/user/repos`; Compose exposes localized signed-out, browser-waiting/cancel, loading, content, empty, offline, permission, expired-auth, callback, and server-error states.
- Files changed: Android manifest and extraction rules, network/auth/repository data layers, repository ViewModel/Compose screen, dependencies/resources, focused tests, and TASK-002B/004/005/011 status documentation.
- Checks and results: Debug APK build passed; 57 unit tests across 16 suites passed; Android lint passed; diff and sensitive-pattern checks passed; no WebView or production client secret exists.
- Review findings and resolutions: Browser cancellation dead-end, HTTP response-read exception handling, signed-in error recovery/logout, nullable DTO validation, immutable UI snapshots, and bounded pagination coverage were fixed. Final independent re-review reported no remaining P0/P1.
- Known limitations: No Android device is connected, so the hosted App Link, system-browser return, live account login, repository result, and device Keystore behavior require one manual E2E run on the target phone.
- Next recommended task: Install the debug APK on the target Android device and verify login plus the internal repository list; after that, begin branch browsing.
- Next owner: Human product owner for device E2E, then Codex.
## 2026-08-25 — TASK-011 device E2E closure
- Agent and role: Human product owner, device verifier; Codex, evidence recorder
- Verification performed: Installed application login against the production company Gitea and retrieval of the authenticated account's internal repository list.
- Result: System-browser OAuth, verified App Link return, authenticated API access, and repository-list rendering passed on the target device.
- Review status: Automated gates previously passed and final independent review reported no P0/P1; the remaining manual device gate is now closed.
- Task status: TASK-011 complete.
- Next recommended task: Add repository selection and branch listing.
- Next owner: Codex.
## 2026-08-25 — TASK-012
- Agent and role: Codex, workflow implementer
- Commit/diff: TASK-012 documentation diff
- Features implemented: Durable current-state and remaining-roadmap handoff, Claude working-style prompt, and bidirectional append-only question/answer mailboxes.
- How it was implemented: Writer-specific directories and immutable per-message files preserve the single-writer protocol; the roadmap orders work by frozen-contract dependencies and assigns one owner per task.
- Files changed: `handoff/`, TASK-006A status, TASK-012, collaboration documentation, and this log.
- Checks and results: Documentation paths, baseline commits, ownership, task dependencies, and mailbox cross-references inspected; no Android code or external infrastructure changed.
- Review findings and resolutions: Pending Claude read-only review of the handoff package.
- Known limitations: Mailboxes coordinate repository-based handoff and do not provide real-time notification; agents must inspect them at task start.
- Next recommended task: Claude implements TASK-006A only and hands its commit to Codex for review.
- Next owner: Claude.
## 2026-08-25 — TASK-012 review closure and activation
- Agent and role: Codex, finding resolver and handoff owner; Claude, independent read-only reviewer
- Commit/diff: reviewed package commit `57d1c09`; immutable activation ref `handoff/task-006a-ready`
- Findings resolved: Canonical delivery sequence now explicitly permits independent TASK-006A before the TASK-004/005 remainder; TASK-006A authorizes only its completion-log append; both mailbox directions share symmetric immutable templates and unique correction filenames; completed OAuth evidence is synchronized.
- Checks and results: Documentation cross-references and task dependencies inspected; `git diff --check` passed before activation; no Android production file or external infrastructure changed.
- Review result: No handoff activation occurs until the tag resolves to the committed metadata state. Claude checks out the tag, creates a `claude/` branch, implements TASK-006A only, and then stops for Codex review.
- Known limitation: Repository mailboxes are asynchronous and must be checked manually at task start.
- Next owner: Claude for TASK-006A.
## 2026-08-25 — TASK-006A implementation
- Agent and role: Claude, assigned feature implementer (writable scope limited to `feature/codebrowser/content/` and this task's evidence/log entries)
- Commit/diff: branch `claude/task-006a-ready` created from activation tag `handoff/task-006a-ready` (base `965d55d`); implementation adds `ContentClassification.kt`, `SourceContentClassifier.kt`, and `SourceContentClassifierTest.kt` under `feature/codebrowser/content/`
- Features implemented: Pure-Kotlin classifier that turns already-loaded `SourceContent` bytes into one of five immutable states — bounded text (with independent line/character truncation flags), binary, Git LFS pointer, unsupported encoding, or oversized — using caller-supplied positive byte/character/line limits.
- How it was implemented: Oversized input is rejected by raw byte count before any decoding; binary is detected by a raw NUL-byte scan; text decoding uses a strict UTF-8 `CharsetDecoder` with `CodingErrorAction.REPORT` for malformed and unmappable input so it never substitutes replacement characters; a Git LFS pointer is recognized only from the exact canonical `version` line followed by valid `oid sha256:<64 hex>` and `size <integer>` lines within the 1024-byte spec bound; remaining text is truncated independently by line count and character count with an explicit `isTruncated` flag. No shared domain, data, Gradle, manifest, or navigation file was modified; `RepositoryModels.kt`/`RepositoryPath.kt`/`RepositoryRef.kt` remain identical to contract-freeze commit `0929282`.
- Files changed: `app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/content/ContentClassification.kt`, `app/src/main/java/com/ubisam/ubgitea/feature/codebrowser/content/SourceContentClassifier.kt`, `app/src/test/java/com/ubisam/ubgitea/feature/codebrowser/content/SourceContentClassifierTest.kt`, `tasks/006a-code-content-classifier.md` (evidence/status), this log entry.
- Checks and results: `./gradlew testDebugUnitTest --tests "com.ubisam.ubgitea.feature.codebrowser.content.*"` passed (25/25); `./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks` passed (82 unit tests repository-wide, 0 failures; lint 0 errors, 22 pre-existing warnings unchanged from TASK-001 baseline); `git diff --check` clean; sensitive-pattern scan of changed files found no secret/token/password/private-key markers.
- Review findings and resolutions: None yet — pending Codex read-only review.
- Known limitations: No Android device/emulator was used, since this task has no Android framework dependency (pure JVM-testable Kotlin); static formatting/check task remains unconfigured for the module.
- Next recommended task: Codex reviews this commit read-only per `docs/workstreams.md` cross-review rules; TASK-006 UI remains blocked until Codex freezes the TASK-004/005 contracts.
- Next owner: Codex, for review.
## 2026-08-26 — TASK-006A Codex integration review
- Agent and role: Codex, integrator and reviewer
- Commit/diff: Claude commit `979729a`, integrated as `4f5faf3`, plus Codex Unicode-boundary fix
- Review findings and resolutions: One P2 was found: UTF-16 substring truncation could split an emoji or another supplementary character into a lone surrogate. Rendering now counts and truncates Unicode code points, with a focused emoji regression test.
- Checks and results: Focused classifier, full unit-test, debug-build, lint, diff, and sensitive-pattern gates are rerun after the fix.
- Known limitations: TASK-006 remains blocked on completion and freeze of the TASK-004/005 API and navigation contracts.
- Next recommended task: Codex completes the TASK-004 branch/tag/tree/file API remainder, then TASK-005 repository selection and branch browsing.
- Next owner: Codex.
## 2026-08-26 — TASK-004
- Agent and role: Codex, implementer and integrator
- Commit/diff: TASK-004 working diff following `ed2121b`
- Features implemented: Authenticated read-only repository, branch, tag, directory-tree, Base64 source-file, and commit API adapter with immutable pagination.
- How it was implemented: OkHttp `HttpUrl` encodes every owner/repository/path segment and ref/path query independently; private nullable Gson DTOs validate before mapping; bearer-token access is injected; contents arrays are locally paged; file type/path/encoding/size are verified; HTTP and network outcomes use typed failures.
- Files changed: Core network failures, repository data adapter/tests, tag domain/use-case contracts, HTTP decision ADR, Gradle test dependency, TASK-004 evidence, and this log.
- Checks and results: Focused in-memory and MockWebServer tests passed; repository-wide debug build and lint passed; 90 unit tests across 19 suites passed with zero failures/errors; diff check and focused sensitive-pattern scan passed.
- Review findings and resolutions: Self-review added page-overflow protection, returned-file path matching, nullable size validation, Unicode-safe prior classifier integration, and a real local HTTP boundary test.
- Known limitations: Gitea contents has no server-side pagination, so later directory pages repeat the request and slice the immutable returned snapshot locally.
- Review status: Codex completed source/contract self-review. Independent review is deferred while the user has assigned the serial implementation stream to Codex and must be completed before TASK-006 contract freeze.
- Next recommended task: TASK-005 repository selection and branch/tag browsing UI.
- Next owner: Codex.
## 2026-08-26 — TASK-005
- Agent and role: Codex, implementer and integrator.
- Commit/diff: TASK-005 working diff following `8a47d30`; the exact completion commit is recorded after commit creation.
- Features implemented: Repository selection plus repository-scoped branch and tag browsing, default/protected/selected reference indicators, abbreviated commit IDs, refresh, retry, logout, and deterministic back navigation.
- How it was implemented: A unidirectional immutable ViewModel state machine consumes the TASK-004 read-only browser contract, loads bounded pages serially, deduplicates references by name, cancels replaced work, and restores the cached repository-list snapshot. Small gateway interfaces make authentication and repository restoration JVM-testable without Android framework construction.
- Files changed: Repository Compose/ViewModel/presenter and tests, MainActivity callback wiring, testable authentication/repository gateway boundaries, localized repository strings, and TASK-005 evidence.
- Checks and results: Focused presenter/ViewModel tests passed; repository-wide debug build, 96 unit tests across 21 suites, and Android lint passed; diff and focused sensitive-pattern checks passed.
- Review findings and resolutions: Self-review added immutable reference snapshots, operation cancellation on back navigation, typed reference errors retaining repository context, and ViewModel tests for selection/restoration/error behavior. Independent review is deferred while Codex owns the serial stream.
- Known limitations: No device/emulator interaction test was available in this work cycle. The reviewed contract-freeze checkbox remains open until independent integration review.
- Next recommended task: TASK-006 directory navigation and bounded source-file viewing using the TASK-004 API and reviewed TASK-006A classifier.
- Next owner: Codex for continuous implementation; Claude may later perform the independent review when re-enabled by the product owner.
## 2026-08-26 — TASK-006
- Agent and role: Codex, temporary feature implementer and integrator while Claude work is paused.
- Commit/diff: TASK-006 working diff based on TASK-005 commit `bbbff77`; exact implementation commit recorded after creation.
- Features implemented: Explicit branch/tag tree browsing, deterministic breadcrumbs and back navigation, directory/file selection, bounded selectable/copyable source text, and dedicated binary/LFS/unsupported-encoding/oversized/symlink/submodule fallbacks.
- How it was implemented: A feature-owned immutable ViewModel state machine passes repository coordinates, ref, and validated path to every TASK-004 read-only request. It consumes bounded pages serially, cancels replaced work, retains file-vs-directory retry intent, and delegates byte classification to the reviewed TASK-006A component. The Compose screen keeps repository/ref/path visible and virtualizes directory rows.
- Files changed: Code-browser ViewModel, navigation helper, Compose screen, Korean resources, focused tests, MainActivity feature wiring, selected-reference handoff, and TASK-006 evidence.
- Checks and results: Focused code-browser tests passed; repository-wide debug build, 102 unit tests across 23 suites, and Android lint passed; diff and focused sensitive-pattern/read-only scans passed.
- Review findings and resolutions: Self-review fixed root-loading back behavior, file retry incorrectly becoming a directory request, file breadcrumbs attempting directory loads, configuration-change screen loss, and non-virtualized directory rendering. Independent review is deferred.
- Known limitations: Physical-device interaction has not yet been rerun. TASK-005's reviewed contract-freeze criterion remains open pending later independent review.
- Next recommended task: TASK-009 immutable history/change/diff API contracts, followed by TASK-007 read-only history and change UI.
- Next owner: Codex for the continuous serial stream.
## 2026-08-26 — TASK-004/005/006 independent review and freeze
- Agent and role: Claude, independent reviewer per `handoff/claude-master-handoff.md`'s "review and freeze" first action; human product owner authorized Claude as incoming primary implementer with handback to Codex kept possible at any time.
- Commit/diff: reviewed `8a47d30` (TASK-004), `bbbff77` (TASK-005), `16c46a5` (TASK-006) read-only, then fixed one finding on branch `claude/review-005-006-freeze` from `16c46a5`.
- Review scope and method: Independently reran `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` (did not trust the recorded evidence alone) and read `GiteaRepositoryBrowserApi.kt`, `HttpTransport.kt`, `RepositoryListViewModel.kt`, `RepositoryReferencePresenter.kt`, `CodeBrowserViewModel.kt`, `CodeBrowserNavigation.kt`, and `MainActivity.kt` against the master handoff's checklist: races/cancellation, root back, rotation restoration, URL encoding, immutable snapshots, page overflow, and 404/409/422 mapping. TASK-009's commit was not yet available (still uncommitted upstream) and was excluded from this pass per human product owner direction.
- Findings:
- P2 (fixed): `core/network/HttpTransport.kt` `requireSuccess()` mapped 401/403/404/429/5xx but let 409 and 422 fall through to the generic `ApiFailure.InvalidResponse`, which would have blocked TASK-007's planned `conflict`/`unprocessable` UDF states. Added `ApiFailure.Conflict`/`ApiFailure.Unprocessable` and extended `GiteaRepositoryBrowserApiTest`'s status-mapping test.
- P3 (recorded, not fixed): `CodeBrowserViewModel` runs JSON parsing, Base64 decoding, directory sort/dedupe, and the TASK-006A classifier on `viewModelScope`'s Main dispatcher after the network suspend point resumes. Not blocking; recommended for TASK-016 (release hardening) given its low-end-device and large-content scope.
- Everything else checked (races/cancellation, root back via `BackHandler` in both screens, immutable snapshot wrapping, page-overflow bounds, URL component encoding) verified correct with no finding.
- Checks and results after the fix: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL, 102 unit tests across 23 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean; sensitive-pattern scan clean (one match was a test asserting `client_secret` absence, not a leak).
- Task evidence updated: TASK-004, TASK-005, and TASK-006 each record this closure. TASK-005's "reviewed completion commit is TASK-006's contract-freeze base" and TASK-006's "evidence records the exact reviewed TASK-005 contract-freeze commit" checkboxes are both closed. TASK-009's corresponding freeze checkbox remains open pending its own commit and review.
- Known limitations: No physical device was available, so device-interaction checks named in TASK-005/006 remain deferred, unchanged from before this review.
- Next recommended task: TASK-007 (history and change UI), per the ordered backlog in `handoff/claude-master-handoff.md`, once TASK-009 lands and is reviewed; TASK-010 (work-item API contracts) ownership is transferred to Claude per the same handoff.
- Next owner: Claude, continuing the primary implementer role with handback to Codex available at any time by flipping a task's Owner field back and picking up from its last recorded commit.
## 2026-08-26 — TASK-017: recent-push sort, paged repository list, last-pushed branch
- Agent and role: Claude, feature implementer, per human product owner priority-1 request in conversation on 2026-08-26.
- Commit/diff: branch `claude/task-017-recent-push-favorites` from `fdda164`; new task file `tasks/017-repository-list-recent-push.md`.
- Features implemented: Repository list is now sorted most-recently-pushed-first (server-side `sort=updated&order=desc`, correct across page boundaries), paged 5 at a time with explicit previous/next navigation, and each visible repository independently shows (and can retry) its most recently committed branch.
- How it was implemented: Added `updatedAtEpochMillis` to `RepositorySummary` and `commitAtEpochMillis` to `BranchSummary`, mapped from Gitea's `updated_at` and branch `commit.timestamp`. Replaced the legacy eager-loading `RepositoryListApi`/`AccessibleRepositoriesGateway` with the existing paged `GiteaRepositoryBrowser.listRepositories`. `RepositoryListViewModel` now tracks a page number and launches one cancellable job per visible row to fetch and compare that repository's branch timestamps; jobs are cancelled on page change/refresh/navigation-away and resumed (not restarted) when returning via back-navigation.
- Files changed: `domain/model/RepositoryModels.kt`, `data/repositories/GiteaRepositoryBrowserApi.kt`, removed `data/repositories/RepositoryListApi.kt` and its test, `feature/repositories/RepositoryListViewModel.kt`, `feature/repositories/RepositoryListScreen.kt`, `MainActivity.kt`, `strings_repositories.xml`, associated tests, `tasks/017-repository-list-recent-push.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 105 unit tests across 22 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean (CRLF notices only); sensitive-pattern scan clean.
- Review findings and resolutions: Self-review caught and fixed two issues before they shipped: (1) page-navigation/refresh calls had no `try`/`catch`, which would have crashed the app on any network failure during `nextPage()`/`previousPage()`/`refreshRepositories()` — wrapped in the same error-mapping pattern used elsewhere; (2) a repository with no default branch (a real Gitea state for empty repos) would have failed the entire page load via the existing fail-closed `.required()` mapping — `defaultBranch` is now nullable at the domain level, matching how the UI already handled it. Independent Codex review is pending.
- Known limitations: No physical device was available to verify page navigation and per-row branch loading interactively. Timestamp ties between two branches resolve deterministically but arbitrarily (first match kept).
- Next recommended task: TASK-018 (Gitea-star favorites tab), already scoped in `tasks/018-favorites-gitea-star.md`, is next; Codex review of TASK-017 can happen in parallel since it touches non-overlapping paths once merged.
- Next owner: Codex for read-only review of TASK-017; Claude continuing to TASK-018.
## 2026-08-26 — TASK-018: Favorites tab backed by Gitea star
- Agent and role: Claude, feature implementer, per human product owner priority-1 request in conversation on 2026-08-26 (same request as TASK-017).
- Commit/diff: same branch as TASK-017 (`claude/task-017-recent-push-favorites`), committed as a separate commit on top of it; new task file `tasks/018-favorites-gitea-star.md`, ADR `docs/decisions/004-favorites-write-user-scope.md`.
- Features implemented: A user can star a repository from the main list and unstar it from a new Favorites tab, which shows only their starred repositories with the same paging/loading/empty/error states as the main list.
- How it was implemented: Added `write:user` to the app's requested OAuth scopes and replaced `OAuthClientConfig`'s "every scope must be read-only" guard with "every scope must be read-only or in an explicit approved-write-scope allow-list" (currently exactly `write:user`) — the app's first non-read-only scope and first write endpoint. `HttpTransport` gained bearer-authenticated `put`/`delete`. `GiteaRepositoryBrowserApi` now also implements a new `RepositoryFavorites` domain interface (`listStarred`/`star`/`unstar`) on the same class, reusing its existing repository-mapping code. `RepositoryListViewModel` gained a `RepositoryListMode` (ALL/FAVORITES) carried on its `Content`/`Empty` states; toggling favorite either marks a row `Starred` (ALL mode, confirmed-not-optimistic) or removes it from the current page (FAVORITES mode, after a confirmed unstar). `RepositoryListScreen` gained a tab switcher and a per-row favorite button using plain localized text (no new icon dependency).
- Files changed: `core/auth/protocol/OAuthClientConfig.kt`, `core/network/HttpTransport.kt`, `data/repositories/GiteaRepositoryBrowserApi.kt`, new `domain/repository/RepositoryFavorites.kt`, `feature/repositories/RepositoryListViewModel.kt`, `feature/repositories/RepositoryListScreen.kt`, `MainActivity.kt`, `strings_repositories.xml`, associated tests, new `OAuthClientConfigTest.kt`, ADR-004, `tasks/018-favorites-gitea-star.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 117 unit tests across 23 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean (CRLF notices only); sensitive-pattern scan clean.
- Review findings and resolutions: Self-review narrowed two acceptance criteria from their original draft to match what was actually and deliberately built: favorite-toggle failures use one generic retryable state rather than per-HTTP-status messages (proportionate for a lightweight toggle), and favorite state updates only after a confirmed server response rather than optimistically. Independent Codex review is pending, and this is the first task to also request on-device verification of the new OAuth scope specifically (does the Gitea instance's OAuth app registration accept it without a server-side change).
- Known limitations: No physical device was available — the new-scope login path is unverified. Existing signed-in users need one fresh login before Favorites works. Favorite state is never cached locally (always read fresh from Gitea).
- Next recommended task: Codex reviews TASK-017 and TASK-018 together (same branch) and verifies the `write:user` login on a physical device before this reaches users; TASK-007 (history UI) remains next in the original roadmap once TASK-009 lands.
- Next owner: Codex, for review and device verification.
## 2026-08-26 — TASK-010: pull-request and issue API contracts
- Agent and role: Claude, feature implementer (ownership transferred by human product owner in `handoff/claude-master-handoff.md`), continuing work per the human product owner's "진행해줘" (proceed) instruction while TASK-009 remained uncommitted on Codex's side.
- Commit/diff: branch `claude/task-010-work-items-contract` from `525a0aa`; new `domain/workitems/` and `data/workitems/` packages.
- Features implemented: Frozen, read-only domain and data contracts for pull requests, issues, reviews, and comments — `listIssues`/`getIssue`/`listIssueComments`, `listPullRequests`/`getPullRequest`/`listPullRequestReviews`/`listPullRequestComments`.
- How it was implemented: `IssueSummary`/`IssueDetail` and `PullRequestSummary`/`PullRequestDetail` are fully separate models. `WorkItemAuthor` is `Known`/`Unavailable` so a deleted or null author never becomes an invented name. `PullRequestState` is derived from Gitea's `merged` flag combined with its open/closed state (there is no single field that already encodes "merged"). `listIssues` drops entries whose `pull_request` field is non-null so Gitea's issue-list endpoint (which also returns PRs) never duplicates what `listPullRequests` already exposes — done client-side rather than trusting a guessed, unverified query parameter. PR/issue discussion comments both reuse `/issues/{number}/comments`, matching how Gitea treats PRs as issues internally. Reused `domain.model.Page<T>` and `core.network.ApiFailure` rather than introducing parallel types.
- Files changed: `domain/workitems/WorkItemModels.kt`, `domain/workitems/WorkItemsRepository.kt`, `data/workitems/GiteaWorkItemsApi.kt`, `data/workitems/GiteaWorkItemsApiTest.kt`, `tasks/010-work-items-api-contract.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 128 unit tests across 24 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean; sensitive-pattern scan clean; grep-confirmed no `put`/`delete`/`postForm` call anywhere in the new API class (read-only, as required).
- Review findings and resolutions: None — self-review found no P0-P3. Independent Codex review and contract-freeze recording are still required before TASK-008 (the UI that consumes this contract) may begin.
- Known limitations: Gitea's exact issue/PR filtering query-parameter behavior was not confirmed against authoritative live documentation; the client-side `pull_request`-field filter is correct regardless, at the cost of potentially shorter-than-requested pages after filtering. No physical device or live Gitea instance was used — coverage is fake-transport/unit-level only, consistent with how TASK-004/009 were verified before their own freeze review.
- Next recommended task: Codex reviews and freezes this contract; TASK-008 (PR/issue UI) can then begin. TASK-007 remains blocked on TASK-009 independently.
- Next owner: Codex, for review and contract-freeze recording.
## 2026-08-26 — TASK-009: history and diff API contracts
- Agent and role: Claude, feature implementer, at the human product owner's explicit direction ("그냥 코덱스 말고 너가 다시 작업해줘" — implement it yourself instead of Codex) rather than continue waiting on Codex's separate, still-uncommitted attempt in the primary worktree.
- Commit/diff: branch `claude/task-009-history-diff-contract` from `7d122d4`; new `domain/history/` and `data/history/` packages. Codex's parallel uncommitted attempt at the same task was not read or referenced, to keep this an independent implementation.
- Features implemented: Commit detail with per-file added/modified/deleted/renamed/other status, bounded commit and comparison patch/diff text with an explicit truncated flag, and ref comparison with locally-computed pagination.
- How it was implemented: Verified Gitea's actual response shapes against its own Go source (`modules/structs/repo_commit.go`, `compare.go` field names) rather than guessing. Deliberately did not duplicate commit-history listing, since TASK-004's `GiteaRepositoryBrowser.listCommits` already provides it. Patch text comes from Gitea's `.diff`-suffixed raw-text endpoints (not the JSON commit/compare objects, which only carry file status, not diff content) and is bounded by byte limit then line limit, both constructor-injected and validated positive. The compare endpoint's single combined `base...head` path segment is built by concatenating both ref values before one `addPathSegment` call, so each ref (including any `/` in a branch name) is escaped exactly once — verified by a test using a ref containing a slash. Comparison pagination is computed locally against Gitea's one-shot compare response, reusing the same slicing pattern TASK-004 already uses for tree listing.
- Files changed: `domain/history/HistoryModels.kt`, `domain/history/HistoryRepository.kt`, `data/history/GiteaHistoryApi.kt`, `data/history/GiteaHistoryApiTest.kt`, `tasks/009-history-diff-api-contract.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 141 unit tests across 25 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean; sensitive-pattern scan clean; grep-confirmed no `put`/`delete`/`postForm` call in the new API class.
- Review findings and resolutions: None — self-review found no P0-P3. Two items flagged for reviewer attention rather than treated as settled: the exact Gitea file-status vocabulary beyond added/modified/deleted/renamed was not exhaustively confirmed, and the `.diff`-suffixed compare route was inferred from the documented single-commit convention rather than independently verified.
- Known limitations: No physical device or live Gitea instance was used. Codex was independently attempting this same task in parallel (uncommitted); this entry and this implementation exist alongside that one and the two need to be reconciled by whoever reviews, not assumed to be the same.
- Next recommended task: Codex (or whoever reviews) reconciles this implementation against its own parallel TASK-009 attempt, reviews, and records the contract-freeze commit so TASK-007 can begin.
- Next owner: Codex, for reconciliation, review, and contract-freeze recording.
## 2026-08-26 — TASK-007: commit history and change UI
- Agent and role: Claude, feature implementer, per the human product owner's direction that Claude owns all subsequent tasks rather than Codex ("이후 다른 작업들도 코덱스 말고 너가 다 작업해줘").
- Commit/diff: branch `claude/task-007-history-ui` from `3c17476` (this session's own TASK-009, not yet Codex-reviewed); new `feature/history/` package plus `MainActivity.kt` and `CodeBrowserScreen.kt` wiring.
- Features implemented: Paginated commit history for the currently browsed ref; commit detail with distinguished added/modified/deleted/renamed file status; bounded, explicitly-truncated patch text for a single commit or a ref comparison; a comparison flow that validates one entered ref against the currently open ref as base.
- How it was implemented: Reuses TASK-004's `listCommits` for history listing and this session's own TASK-009 `HistoryRepository` for detail/patch/compare — no shared/domain file touched. A commit's file list and its patch load as two independently-retryable steps rather than one blocking fetch. Entry point is a new "히스토리" button added to the existing code browser header, wired through a third `browsingHistory` screen flag in `MainActivity` layered above the existing `browsingCode` flag.
- Files changed: `feature/history/HistoryViewModel.kt`, `feature/history/HistoryScreen.kt`, `strings_history.xml`, `MainActivity.kt`, `feature/codebrowser/CodeBrowserScreen.kt`, `strings_codebrowser.xml`, `tasks/007-history-and-changes.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 150 unit tests across 26 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean; sensitive-pattern scan clean.
- Review findings and resolutions: Self-review caught and fixed one real bug before writing tests: a failed commit-detail fetch discarded which commit had failed, so "retry" silently fell back to reloading the whole list instead of re-fetching the commit the user actually tried to open. The `Error` state now retains the failed `CommitSummary` so retry targets the same commit.
- Known limitations: TASK-009 has not been independently reviewed or contract-frozen — this task consumes it directly, with no cross-review from a second implementer, per explicit human direction. No physical device was available to verify the three-level back stack (repositories → code browser → history) interactively. Comparison always uses the open ref as base (no arbitrary base-ref picker); comparison patch is fetched once for the whole range, not per commit.
- Next recommended task: TASK-010 is already done (this session); TASK-008 (PR/issue UI) is next and consumes it the same way TASK-007 consumed TASK-009 — directly, without waiting for Codex review, per the same standing direction.
- Next owner: Claude, continuing to TASK-008.
## 2026-08-26 — TASK-008: pull-request and issue UI
- Agent and role: Claude, feature implementer, continuing the standing direction that Claude owns all subsequent tasks without stopping to ask between them.
- Commit/diff: branch `claude/task-008-workitems-ui` from `429668b`; new `feature/workitems/` package plus `MainActivity.kt` and `RepositoryListScreen.kt` wiring.
- Features implemented: Separate issue and PR list/detail views (tabs), paginated, with comments (issues) and reviews+comments (PRs); deleted/unavailable authors render a fixed placeholder instead of crashing or inventing identity; every status is a text label, never color-only.
- How it was implemented: Reuses TASK-010's domain contracts directly — no shared/domain file touched. Both list types apply `distinctBy(number)` per page and use the same discrete prev/next paging convention as TASK-017/007, which structurally prevents duplicate rows without separate merge logic. Entry point is a new button on the repository references screen (issues/PRs are repository-scoped, not ref-scoped, unlike history).
- Files changed: `feature/workitems/WorkItemsViewModel.kt`, `feature/workitems/WorkItemsScreen.kt`, `strings_workitems.xml`, `MainActivity.kt`, `feature/repositories/RepositoryListScreen.kt`, `strings_repositories.xml`, `tasks/008-work-items-ui.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 158 unit tests across 27 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean; sensitive-pattern scan clean.
- Review findings and resolutions: None — self-review found no P0-P3.
- Known limitations: TASK-010 has not been independently reviewed or contract-frozen. No physical device or instrumented Compose test verified accessibility semantics interactively (consistent with every other feature this session). Pagination dedupe is verified within a single page's response, not across an accumulating infinite scroll.
- Next recommended task: This closes the originally-scoped TASK-007/008 pair. Remaining roadmap items (TASK-013 navigation, TASK-014 organizations, TASK-015 notifications, TASK-016 release hardening) have no task files yet.
- Next owner: Claude.
## 2026-08-26 — TASK-013: typed application navigation
- Agent and role: Claude, feature implementer, continuing the standing direction to own all subsequent tasks without asking between them. No task file existed for TASK-013 beforehand (only proposed in an earlier handoff document); created it first per `AGENTS.md`.
- Commit/diff: branch `claude/task-013-navigation` from `c397fbe`; new `ui/navigation/` package, rewrote `MainActivity.kt`.
- Features implemented: Replaced `MainActivity`'s three independent booleans with one typed `List<Route>` back stack, restorable through configuration change and process death.
- How it was implemented: `Route` is a sealed type of stable primitives only (owner/name/ref/ref-kind — never tokens or content). `RouteStackSaver` encodes each route as a delimiter-joined string using U+001F, a character `RepositoryCoordinates`/`RepositoryRef` already guarantee can never appear in real field values. A single `LaunchedEffect(currentRoute)` re-triggers the owning feature ViewModel's load whenever the top route changes, covering both fresh navigation and restoration identically. This is a genuine behavior improvement, not just a refactor: the stack is a real LIFO, so back correctly unwinds through arbitrarily nested screens instead of relying on which boolean happened to still be `true` underneath another.
- Files changed: `ui/navigation/Route.kt`, `ui/navigation/RouteStackSaver.kt`, `RouteEncodingTest.kt`, `MainActivity.kt`, `tasks/013-typed-navigation.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 164 unit tests across 28 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean; sensitive-pattern scan clean.
- Review findings and resolutions: None functional. One implementation detail worth recording: Compose's `listSaver` actually returns `Saver<List<T>, Any>`, not `Saver<List<T>, List<Saveable>>` as its type parameters suggest — found via the compiler's own type-mismatch error rather than assumed, and `RouteStackSaver`'s declared type corrected to match.
- Known limitations: Only the route itself survives process death, not deeper in-feature state (open commit, scroll position) — explicitly out of scope. No physical device was available to verify process-death restoration interactively; the `Saver` round-trip is unit-tested directly instead.
- Next recommended task: TASK-014 (organizations) and TASK-015 (notifications) are real MVP scope items from `docs/product.md` with no task files yet; TASK-016 (release hardening) depends on those and needs a physical device. Continuing per standing direction.
- Next owner: Claude.
## 2026-08-26 — TASK-014: organizations and organization repository filter
- Agent and role: Claude, feature implementer, continuing the standing direction to own all subsequent tasks. No task file existed beforehand; created it first per `AGENTS.md`. Closes MVP outcome #2 in `docs/product.md` ("View the signed-in user and organizations").
- Commit/diff: branch `claude/task-014-organizations` from `7756ec4`; new `domain/organizations/` package, additive changes to `GiteaRepositoryBrowserApi`/`RepositoryListViewModel`/`RepositoryListScreen`.
- Features implemented: The repository list can be filtered to one organization (a horizontally-scrollable chip row: 전체 + one per org) and back, keeping the existing recent-push sort and paging.
- How it was implemented: Verified Gitea's actual `Organization` struct (`name`/`full_name`) before implementing. `GiteaRepositoryBrowserApi` gained a third interface (`OrganizationsRepository`), reusing its existing `mapRepository` for org-scoped repos rather than duplicating mapping. The organizations list itself is fetched once per session and cached, since it changes rarely and every page load re-fetching it would be wasteful; a failure to load it degrades to an empty filter row rather than blocking the repository list, which is the primary content.
- Files changed: `domain/organizations/OrganizationModels.kt`, `domain/organizations/OrganizationsRepository.kt`, `GiteaRepositoryBrowserApi.kt`, `GiteaRepositoryBrowserApiTest.kt`, `RepositoryListViewModel.kt`, `RepositoryListViewModelTest.kt`, `RepositoryListUiStateTest.kt`, `RepositoryListScreen.kt`, `strings_repositories.xml`, `MainActivity.kt`, `tasks/014-organizations.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 169 unit tests across 28 suites, 0 failures/errors, 0 lint errors; `git diff --check` clean; sensitive-pattern scan clean.
- Review findings and resolutions: None — self-review found no P0-P3.
- Known limitations: No physical device or real multi-organization account was available to verify interactively. Organization membership is read once per session, not refreshed mid-session (matches the app's existing no-background-refresh posture).
- Next recommended task: TASK-015 (notifications) is the last remaining MVP-scope item without a task file; TASK-016 (release hardening) depends on it and needs a physical device.
- Next owner: Claude.
## 2026-08-26 — TASK-015: read-only notifications
- Agent and role: Claude, feature implementer, continuing the standing direction to own all subsequent tasks without stopping to ask between them ("따로 말하지말고 알아서 계속 다음 작업 진행해줘"). No task file existed beforehand; created it first per `AGENTS.md`. Closes the last remaining MVP-scope item from `docs/product.md`.
- Commit/diff: branch `claude/task-015-notifications` from `4fe3bf1`; new `domain/notifications/`, `data/notifications/`, `feature/notifications/` packages, additive changes to `Route.kt`/`RouteStackSaver.kt`/`MainActivity.kt`/`RepositoryListScreen.kt`.
- Features implemented: A signed-in user can view their Gitea notifications (unread-only, the endpoint's own default, or all), paginated, and tap one whose subject is a navigable issue/PR to jump to that repository's work items. No mark-read/unread/pinned write endpoint is reachable from any new code — deliberately excluded, same posture as the star/unstar write scope decision (ADR-004).
- How it was implemented: Verified Gitea's actual `NotificationThread`/`NotificationSubject` struct shape via its own Go source before implementing. Subject type/state fall back to `OTHER`/`UNKNOWN` on an unrecognized value instead of failing closed. The subject's issue/PR number has no dedicated field in the API — it is parsed from the trailing path segment of the subject URL, yielding `null` (not a guess) when unparseable, which is exactly the case for commit/repository-type subjects. Unread state renders as a `[읽지 않음]` text label, never color alone. Self-review caught the same class of bug as TASK-007's history-retry issue: the first draft's `Error` state didn't retain which filter was active, so retrying after a failure while viewing "전체" would have silently reset to "읽지 않음" — fixed by adding `unreadOnly` to `Error`, with a regression test.
- Files changed: `domain/notifications/NotificationModels.kt`, `domain/notifications/NotificationsRepository.kt`, `data/notifications/GiteaNotificationsApi.kt`, `data/notifications/GiteaNotificationsApiTest.kt`, `feature/notifications/NotificationsViewModel.kt`, `feature/notifications/NotificationsScreen.kt`, `feature/notifications/NotificationsViewModelTest.kt`, `strings_notifications.xml`, `ui/navigation/Route.kt`, `ui/navigation/RouteStackSaver.kt`, `MainActivity.kt`, `feature/repositories/RepositoryListScreen.kt`, `tasks/015-notifications.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 188 unit tests total, 0 failures/errors, 0 lint errors; `git diff --check` clean; sensitive-pattern scan clean.
- Review findings and resolutions: None beyond the retry-filter bug caught and fixed during self-review (see above). One item flagged for reviewer attention rather than treated as settled: applying `status-types=unread&status-types=read` to the notification-list endpoint to request "전체" was not independently verified against a live Gitea instance — Gitea's public docs describe that parameter only on the per-repo mark-read endpoint; if wrong, the fallback is two separate calls merged client-side.
- Known limitations: Tapping an issue/PR notification opens that repository's work-item **list**, not the specific item's detail — `Route.WorkItems` (TASK-013) carries no item number, and adding one was out of this task's scope. No physical device was available to verify interactively.
- Next recommended task: TASK-016 (release hardening) is the last item on the proposed roadmap; it depends on TASK-014/015 (both now done) and was previously noted as needing a physical device, which remains unavailable in this environment — will surface that as a genuine blocker rather than skip it silently.
- Next owner: Claude.
## 2026-08-26 — TASK-016: release hardening (partial — device-dependent items explicitly blocked)
- Agent and role: Claude, feature implementer, continuing the standing direction to own all subsequent tasks. No task file existed beforehand; created it first per `AGENTS.md`.
- Commit/diff: continued on `claude/task-015-notifications` (no separate branch cut for this small follow-on); changes to `CodeBrowserViewModel.kt`/`CodeBrowserViewModelTest.kt`, new `tasks/016-release-hardening.md`.
- Features implemented: Fixed the P3 dispatcher issue recorded during the TASK-004/005/006 review — `CodeBrowserViewModel` now runs directory listing/dedupe/sort and file fetch+classification on an injectable `defaultDispatcher` (`Dispatchers.Default` in production) instead of `viewModelScope`'s Main dispatcher.
- How it was implemented: An AVD (`WhatToEat_API_36`) turned out to be available in this environment — confirmed with the human product owner before using it, after they clarified an emulator should exist (Codex had used one on a different project). Booted it and ran a pre-authentication smoke test: cold install, launch, sign-in screen renders correctly (localized Korean text), tapping "Gitea로 로그인" correctly hands off to the system browser toward the OAuth flow, no crash. Attempting to click through Chrome's first-run screen to test further hit a hard constraint, not a tooling gap: completing Gitea login requires entering a password, which is a universally prohibited action in this agent's safety rules regardless of whose credentials are offered — one blind tap nearly walked into Chrome's "add a Google account" flow, which was backed out of immediately (`am force-stop`) without proceeding. Confirmed with the human product owner (2026-08-26) how to proceed: do everything verifiable without login, leave the rest explicitly blocked rather than attempted or silently skipped. Also ran a full-repository secret scan (not scoped to one diff), reviewed the manifest/backup-rules/token-vault security posture (all already correct — `allowBackup="false"`, `data_extraction_rules.xml` excludes all sharedpref data, AES/GCM via Android Keystore, no `Log.*`/`println` anywhere in `app/src/main`), and confirmed `assembleRelease` itself builds. Deliberately did **not** enable R8/ProGuard minification: every `data/*Api.kt` class uses Gson with `TypeToken<List<T>>` generic reflection, which R8 can silently break without correct keep rules, and the only way to catch that here is exercising the authenticated, JSON-parsing-heavy screens on a minified build — exactly what login being unreachable prevents verifying.
- Files changed: `feature/codebrowser/CodeBrowserViewModel.kt`, `feature/codebrowser/CodeBrowserViewModelTest.kt`, `tasks/016-release-hardening.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL — 182 unit tests across 30 suites, 0 failures/errors, 0 lint errors; `assembleRelease` BUILD SUCCESSFUL (`lintVitalRelease` passed); full-repository secret-pattern scan clean (only the pre-existing test asserting `client_secret` absence); `git diff --check` clean.
- Review findings and resolutions: None beyond the P3 fix itself, which was already a recorded, pre-existing finding from an earlier review, not discovered fresh in this pass.
- Known limitations (both recorded as explicitly blocked acceptance criteria, not silently skipped): (1) R8/ProGuard minification was not enabled — left as an open decision for whoever has real device + login access to verify safely. (2) On-device regression of every authenticated screen built this session (repository list/favorites/organizations, code browser, history, work items, notifications, navigation back-stack, logout) could not be performed — structurally blocked on this agent never entering a password, confirmed with the human product owner rather than assumed.
- Next recommended task: This closes the originally-proposed roadmap (TASK-007 through TASK-016). Remaining work is exactly the two blocked items above, both requiring a human (or an agent with real login access) at a real device: complete the authenticated-screen regression pass, then make an informed call on enabling minification with real verification against a build that has actually exercised the JSON-heavy screens.
- Next owner: Human product owner or Codex, for the device-and-login-dependent items above; otherwise no further roadmap items remain unassigned.
## 2026-08-26 — TASK-019 OAuth callback crash recovery
- Agent and role: Codex, incident resolver; Claude diagnostic changes preserved.
- Commit/diff: working diff on `claude/task-015-notifications` pending commit.
- Features implemented: Restored the production OAuth request to its previously device-verified read-only scopes; added safe callback exception recovery, non-secret debug reason tags, and defensive saved-route decoding.
- How it was implemented: `write:user` is no longer part of `UBGiteaOAuthDefaults`; callback storage/validation failures are caught at the ViewModel coroutine boundary and become `INVALID_CALLBACK`; malformed or truncated route records are discarded and fall back to `Repositories`.
- Checks and results: Focused OAuth/repository/navigation tests passed; debug build, 184 unit tests across 30 suites, and lint passed; diff and focused secret checks passed.
- Review findings and resolutions: P1 target-server authorization incompatibility from TASK-018 scope expansion was resolved by suspending write scope. P1 repeat-launch crash risk from unchecked saved-route indexes was fixed with shape validation. Callback exception regression coverage was added.
- Known limitations: Physical-device callback verification is still required. Gitea star/unstar mutation is intentionally unavailable with the restored read-only token.
- Next recommended task: Install the rebuilt debug APK, clear UBGitea app data once, complete login approval, and verify repository loading.
- Next owner: Human product owner for device verification, then Claude for review.
## 2026-08-26 — TASK-020 repository-list crash fix
- Agent and role: Codex, incident resolver.
- Root cause: The repository `LazyColumn` used `RepositoryCoordinates` as its item key. Compose passes lazy-item keys to Android saved state, but this domain object is not a `Bundle`-saveable type, causing an uncaught main-thread `IllegalArgumentException` as soon as authenticated rows rendered.
- Implementation: Repository rows now use the stable string key `owner/name`; repository components cannot contain `/`, so the key remains unique across owners and repositories while being Android-saveable.
- Regression coverage: Added tests for deterministic key encoding and for equal repository names under different owners.
- Checks: Focused tests and `assembleDebug testDebugUnitTest lintDebug` passed. The APK installed wirelessly over the authenticated device, `MainActivity` remained resumed, and the cleared crash buffer stayed empty. Final visual inspection is pending because the handset locked.
## 2026-08-26 — TASK-021 global recent-update repository order
- Agent and role: Codex, implementer and device verifier.
- Root cause: This Gitea server returned `/user/repos` in apparent name order despite the existing `sort=updated&order=desc` query, so trusting server paging could not guarantee that the newest repository appeared on page 1.
- Implementation: The ViewModel now loads all server pages in 50-row batches, performs one stable descending sort by `RepositorySummary.updatedAtEpochMillis`, and slices that immutable snapshot into five-row UI pages. Snapshots are scoped by all/favorites/organization selection, reused for next/previous navigation, and invalidated by refresh and logout.
- Checks: Focused regression tests and `assembleDebug testDebugUnitTest lintDebug` passed. The rebuilt APK was installed wirelessly; the authenticated first page visibly changed from alphabetical order to recent-update order, all five rows loaded their latest branch labels, and the crash buffer remained empty.
## 2026-08-26 — TASK-022: tolerate incomplete Gitea branch timestamps
- Agent and role: Claude, picked up Codex's paused in-progress work in the same worktree (`task-015-notifications`) after the human product owner explicitly confirmed it was safe to continue rather than assuming so.
- Commit/diff: continued on `claude/task-015-notifications`; `domain/model/RepositoryModels.kt`, `data/repositories/GiteaRepositoryBrowserApi.kt`, `feature/repositories/RepositoryListViewModel.kt` and three test files.
- Features implemented: A repository with a branch whose commit timestamp is missing or malformed no longer fails the whole branch page (row-level latest-branch fetch or the repository reference screen). Root cause (per Codex's incident evidence, repo `CSOT-T8-iPTN-AMA`): `listBranches`'s mapper required every `commit.timestamp` even though a timestamp isn't needed to identify or browse a branch.
- How it was implemented: Codex had already landed the core fix (`BranchSummary.commitAtEpochMillis` → nullable, mapper tolerates a missing/malformed value instead of throwing, latest-branch picker filters to non-null timestamps before taking the max) plus two of the needed tests, uncommitted, when this was picked up. Verified `RepositoryReferencePresenter` needed no change (it never reads the timestamp field) and added an explicit test proving that rather than leaving it implicit. Found and filled one real gap in the in-progress test coverage: a repository whose branches are *all* missing timestamps wasn't tested — confirmed the existing code already handles it correctly (reports `LastPushedBranchUiState.Failed`, distinct from `Empty`, rather than silently showing nothing) and added the missing test.
- Files changed: `domain/model/RepositoryModels.kt`, `data/repositories/GiteaRepositoryBrowserApi.kt`, `feature/repositories/RepositoryListViewModel.kt`, `data/repositories/GiteaRepositoryBrowserApiTest.kt`, `feature/repositories/RepositoryReferencePresenterTest.kt`, `feature/repositories/RepositoryListViewModelTest.kt`, `tasks/022-tolerate-branch-timestamp-gaps.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors; `git diff --check` clean; secret-pattern scan of the staged diff clean.
- Review findings and resolutions: One gap found and fixed in Codex's in-progress test coverage (the all-unknown-timestamps case) — see above.
- Known limitations: Not retested live against `CSOT-T8-iPTN-AMA` (or any repository with a real missing/malformed branch timestamp) on a physical device with this exact fix — Codex reproduced and diagnosed the original crash on-device, but this fix itself has only unit-test coverage so far.
- Next recommended task: A physical-device retest of this fix (Codex has working wireless-adb device access per TASK-020/021's evidence). No other roadmap items are open.
- Next owner: Codex or the human product owner, for the device retest; otherwise no further items are unassigned.
## 2026-08-26 — TASK-023: tolerate a literal JSON null list response
- Agent and role: Claude, live incident diagnosis and fix, with the human product owner's own device as the reporting/verification device (their first real end-to-end use of the app this session).
- Commit/diff: continued on `claude/task-015-notifications`; `data/repositories/GiteaRepositoryBrowserApi.kt`, `data/repositories/GiteaRepositoryBrowserApiTest.kt`.
- Features implemented: A `parseList`-backed endpoint (branches, tags, commits, repository listing) whose response body is the literal JSON `null` — Go's `encoding/json` serializes a `nil` slice this way instead of `[]` — now maps to an empty list instead of throwing `ApiFailure.InvalidResponse()`. Previously this showed a misleading "Gitea 서버에서 오류가 발생했습니다" (server error) message on a completely successful `200 OK` response.
- How it was diagnosed: The human product owner set up wireless `adb` debugging to their own physical device this session (paired via `adb pair`/`adb connect`); this was used to install successive debug builds directly and read `logcat` live rather than round-tripping screenshots. First confirmed via the Gitea server's own Container Manager request log that the failing `branches` call was a genuine `200 OK` in ~7ms, ruling out an actual server-side 5xx despite the app's own error message claiming one. Then added three rounds of temporary `BuildConfig.DEBUG`-gated `Log.e` diagnostics (the caught exception, per-item mapping detail, then the raw response body) to narrow the cause down to `bodyPreview=null` — a literal `null` string, not empty or malformed JSON. All diagnostic logging was removed before committing; only the real fix and a matching test remain.
- Files changed: `data/repositories/GiteaRepositoryBrowserApi.kt`, `data/repositories/GiteaRepositoryBrowserApiTest.kt`, `tasks/023-tolerate-null-list-responses.md`.
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors; `git diff --check` clean; secret-pattern scan clean. Verified live: reinstalled over wireless `adb` and the human product owner confirmed `CSOT-T8-iPTN-AMA` now opens correctly.
- Review findings and resolutions: The fix was made at the shared `parseList` helper level, not narrowly in `listBranches`, since the same Go nil-slice-to-null serialization can equally affect tags, commits, or repository listing — narrowing it to only the reported endpoint would have left the same latent bug in every sibling endpoint.
- Known limitations: `CSOT-T8-MACRO_SA` (the other repository that showed the same symptom) was not individually re-confirmed by the human product owner, though it goes through the identical code path.
- Next recommended task: None outstanding on the proposed roadmap. TASK-016's two previously-blocked items (full authenticated-screen device regression, R8/ProGuard minification decision) are now more tractable, since a real device session with working wireless `adb` and the human product owner's active participation is established for the first time this session.
- Next owner: Claude, continuing per the standing direction, or the human product owner if they'd rather drive further device testing directly.
## 2026-08-27 — TASK-024: history screen redesign (branch tap, colored selection, commit time, file diff, Z-Fold split view, compare removal)
- Agent and role: Claude, implementing a connected series of live, iterative requests from the human product owner while they tested the app in real time on their own Galaxy Z Fold over the wireless `adb` connection paired in the previous session.
- Commit/diff: continued on `claude/task-015-notifications`; see `tasks/024-branch-tap-opens-history.md` for the full file list — spans `feature/repositories/`, `feature/history/`, `data/history/`, `domain/history/` (new `UnifiedDiffParser.kt`), `domain/model/RepositoryModels.kt`, `ui/DateTimeFormatting.kt` (new), and `MainActivity.kt`.
- Features implemented, in the order requested live: (1) tapping a branch/tag opens commit history instead of the file browser; (2) the selected reference row is color-coded instead of a "선택됨" text label; (3) branch/tag rows and commit rows show commit date/time with an explicit time zone; (4) tapping a changed file shows a parsed before/after diff (no extra network call — parsed client-side from the already-fetched combined patch); (5) the two diff panels scroll vertically in sync and share one horizontal scroll position; (6) diff lines are colored by text only (red/green), not background; (7) on a ≥600dp-wide screen (covers a Z Fold's inner screen unfolded) the file list and selected diff show side by side instead of the diff replacing the list; (8) the entire branch/tag compare feature was removed at the human product owner's request, since it wasn't going to be used.
- How it was implemented: Folded file-diff selection into `CommitDetailContent` as an in-place field (`selectedFile: SelectedFileDiff?`) rather than a separate navigational state, specifically so the wide-layout split view could show the list and the diff from one piece of state without duplicating any fetch logic. Along the way, found and fixed two real bugs via live device debugging: `getCommitDetail` was requesting `/repos/{o}/{r}/commits/{sha}` instead of `/repos/{o}/{r}/git/commits/{sha}` (the same path family the already-working `.diff` endpoint uses), which had been causing every commit tap to 404 with "커밋이나 기준을 찾을 수 없습니다" — found by checking the Gitea container's own request log first (ruling out a real server error) then bisecting with temporary debug-only logging. Separately, a horizontal-scroll bug reported live ("글자 한줄만 스크롤 되게 하지 말고") was fixed by replacing each diff line's independent `rememberScrollState()` with one shared `ScrollState` per panel.
- Files changed: see `tasks/024-branch-tap-opens-history.md`'s Scope section for the full list (13 modified files, 4 new files, 1 new test directory).
- Checks and results: `assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL at every checkpoint through this work (verified repeatedly, not just once at the end), most recently after the compare removal — all unit tests passing, 0 lint errors. `git diff --check` and secret-pattern scan clean.
- Review findings and resolutions: None from a separate review pass — self-review happened continuously via live device testing throughout, which is how items 5 and 8's underlying bugs (commit-detail 404, single-line horizontal scroll) were actually caught, rather than found afterward.
- Known limitations: File browsing (`Route.CodeBrowser`) is now unreachable from the UI — the reference screen was its only entry point, and item 1 redirected that tap to history instead. This is disclosed in the task file as a known, explicit side effect, not an oversight; a follow-up task (requested live, not yet started) will reintroduce it as a "Files" tab alongside "History" on the branch/tag detail screen. One live-reported rendering glitch (two screens appearing to overlap momentarily) has not been independently reproduced or root-caused. A final, single closing device-confirmation pass covering every item above together has not yet been explicitly requested and received.
- Next recommended task: Several more live requests are already queued from the same testing session: a tabbed Files/History branch-detail screen (bringing `Route.CodeBrowser` back into reach), an image-file viewer in the code browser, numbered pagination with direct page-entry for the Issues/PR list (repository/history/notification lists keep prev/next, since Gitea doesn't provide a total count for those endpoints), swipeable 전체/즐겨찾기 tabs on the repository list, and showing each repository row's last-commit time.
- Next owner: Claude, continuing through the queued live requests above.
## 2026-08-27 — TASK-025: tabbed branch detail screen, repository commit time, swipeable repo/favorites tabs, and six live-testing follow-up fixes
- Agent and role: Claude, continuing the same live testing session, implementing the previously-queued Files/History tab request plus a further rapid sequence of live requests and bug reports from the human product owner while they kept testing on their own Galaxy Z Fold over wireless `adb`.
- Commit/diff: continued on `claude/task-015-notifications`; see `tasks/025-branch-detail-tabs.md` for the full file list. `Route.CodeBrowser`/`Route.History` replaced by one `Route.BranchDetail`; new `feature/branchdetail/BranchDetailScreen.kt` and `strings_branchdetail.xml`; touch-ups across `feature/history/HistoryScreen.kt`, `feature/codebrowser/CodeBrowserScreen.kt`, `feature/notifications/NotificationsScreen.kt`, `feature/workitems/WorkItemsScreen.kt`, `feature/repositories/RepositoryListScreen.kt`, and their string resources.
- Features implemented: (1) tapping a branch/tag now opens one screen with two swipeable tabs, 파일 (Files, the restored TASK-006 code browser) and 히스토리 (History), instead of jumping straight to history as TASK-024 left it; (2) each repository row shows its last-commit time, reusing the existing `updatedAtEpochMillis` field; (3) the repository list's 전체/즐겨찾기 tabs are now swipeable, not just tap-to-switch; (4) fixed a live-reported overlapping-screen rendering bug in the commit-detail narrow layout (root cause: a `BoxWithConstraints` branch had multiple children as bare siblings instead of wrapped in a `Column`, so they stacked like `Box` children instead of arranging vertically); (5) fixed `BranchDetailScreen`'s tab row rendering jammed against the status bar (it was the only top-level screen without its own `Scaffold` or explicit inset padding); (6) fixed horizontal scroll not working in the diff view (`fillMaxWidth()` before `horizontalScroll()` plus missing `softWrap = false` meant lines wrapped instead of overflowing); (7) simplified the references-loading message to a plain "로딩 중"; (8) removed every in-app screen-header back button in favor of the system back gesture already wired via each screen's `BackHandler`, including deleting `WorkItemsScreen`'s back-button-only `DetailHeader` composable entirely; (9) replaced manual "새로고침" buttons on the repository and references lists with `PullToRefreshBox` pull-to-refresh.
- How it was implemented: Item 1's `BranchDetailScreen` wraps the existing `CodeBrowserScreen`/`HistoryScreen` unmodified in a `TabRow` + `HorizontalPager`, sharing one exit point (`onCodeBrowserBack`/`onHistoryBack` both close both ViewModels and pop the route) so their own internal `BackHandler`s never need extra coordination. Items 4-6 were each diagnosed from a live bug report (two of them via screenshot) rather than found proactively — item 4 specifically required disproving an initial wrong hypothesis (an `adb` force-stop/restart rendering artifact) after the human product owner reproduced it a second time following a clean manual relaunch, then confirming via `adb shell dumpsys activity` that only one window existed before looking at the Compose layout code itself. Item 9 hit a compile error (`PullToRefreshBox` is a Material3 experimental API) resolved by adding `@OptIn(ExperimentalMaterial3Api::class)` to both composables that use it.
- Files changed: see `tasks/025-branch-detail-tabs.md`'s Scope section for the full list.
- Checks and results: `./gradlew compileDebugKotlin` BUILD SUCCESSFUL (confirming the `PullToRefreshBox` OptIn fix); `./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks` BUILD SUCCESSFUL in 3m 22s, 53 actionable tasks executed, all unit tests passing, 0 lint errors. Build installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass yet — self-review happened via live device testing, which is how items 4 and 6's underlying bugs were actually caught. The `repositories_back` string was deliberately kept (not removed alongside the other `*_back` strings) since it's a distinct error-state recovery action, not a header back button.
- Known limitations: Full device confirmation of this whole batch together (tabs, swipe, overlap fix, status-bar padding, diff scroll, back-button removal, pull-to-refresh) in one pass is still pending as of this commit — the individual fixes were each verified against their own specific report, but not yet re-confirmed together. Two items from the queued list are still not started: an image-file viewer in the code browser, and numbered pagination with direct page-entry for the Issues/PR list.
- Next recommended task: Get the human product owner's live confirmation of this batch, then pick up the two remaining queued items (image viewer, Issues/PR numbered pagination).
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-025 follow-up: fix diff-view horizontal-scroll jitter
- Agent and role: Claude, same live testing session, responding to a screenshot-reported bug immediately after the TASK-025 batch above was installed.
- Commit/diff: continued on `claude/task-015-notifications`; `feature/history/HistoryScreen.kt` only.
- Bug: after TASK-025's first diff-scroll fix (removing `fillMaxWidth()`, adding `softWrap = false`), the human product owner reported that scrolling the diff view no longer failed to scroll, but instead made the text visibly jitter/snap ("코드에서 스크롤하니까 좌우 스크롤이 되는게 아니라 무슨 글자가 춤을 추네" — "the letters are dancing"), shown via screenshot.
- Root cause: `DiffSideLine` had every row in both diff panels sharing one `ScrollState` via `Modifier.horizontalScroll(state)`. A `ScrollState`'s max-scroll bound is set from whichever composable using it was most recently measured; with many rows of different line lengths in the same `LazyColumn`, each row's own layout pass overwrote that shared bound with its own (usually shorter or longer) width, so as different rows got remeasured during scrolling the bound — and therefore the effective clamp on the current scroll position — kept changing out from under the drag, snapping the visible text back and forth.
- Fix: replaced the shared `ScrollState` with a hand-driven offset: a single `MutableFloatState` updated by `rememberScrollableState { delta -> ... }` and clamped against one bound computed once per selected file (via `rememberTextMeasurer()`, measuring the longest line's character count against the diff's monospace style) instead of recomputed per row on every layout pass. Each row now uses `Modifier.scrollable(...)` purely for gesture capture and a plain `Modifier.offset { ... }` for rendering, wrapped in `Box(Modifier.fillMaxWidth().clipToBounds())` since manual offset — unlike `horizontalScroll` — doesn't clip overflow by itself.
- Files changed: `feature/history/HistoryScreen.kt`, `tasks/025-branch-detail-tabs.md` (evidence updated in place rather than as a new task file, since this is a direct continuation of that task's item 6).
- Checks and results: `./gradlew compileDebugKotlin` BUILD SUCCESSFUL; `./gradlew assembleDebug testDebugUnitTest lintDebug` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors. Reinstalled on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass — self-diagnosed from the live bug report and Compose's documented `ScrollState`/layout-bound semantics.
- Known limitations: The upper scroll bound is measured once per file (via a repeated single-character string of the longest line's length), not re-measured if the panel is resized mid-session (e.g., device rotation) — not expected to matter in practice since a file's selected diff is re-entered fresh on any real layout change, but noted for completeness. Live confirmation of this specific fix (does the jitter actually stop) is pending as of this commit, same as the rest of the TASK-025 batch.
- Next recommended task: Get the human product owner's live confirmation of the diff view specifically (in addition to the rest of the TASK-025 batch), then continue to the two remaining queued items.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-026: run OAuth login/consent in a Custom Tab instead of the external browser
- Agent and role: Claude, same live testing session, responding to a direct request after the human product owner re-paired their device over wireless `adb` under a fresh pairing code (the previous session's pairing had expired).
- Commit/diff: continued on `claude/task-015-notifications`; `MainActivity.kt`, `app/build.gradle.kts`, `gradle/libs.versions.toml`, `tasks/026-oauth-custom-tabs.md`.
- Feature implemented: tapping login previously opened the OAuth authorization URL via a plain `Intent.ACTION_VIEW`, switching fully away to a separate browser app for both the Gitea login form and the "Authorize this application" consent screen, then handing control back to UBGitea via the existing redirect-URI intent filter. It now opens in a Chrome Custom Tab (`androidx.browser.customtabs.CustomTabsIntent`) instead, which overlays the calling activity's own task rather than switching to a separate browser app/task — visually stays "in the app" through login and redirect back.
- How it was implemented: added the `androidx.browser:browser` dependency (new to this project) and swapped `openSystemBrowser`'s plain `Intent.ACTION_VIEW` for `CustomTabsIntent.Builder().build().launchUrl(...)`, renaming the method to `openAuthorizationTab` to match what it now does; the `ActivityNotFoundException` → `viewModel.browserLaunchFailed()` fallback was preserved unchanged. Deliberately did not implement this with an in-app `WebView` loading the login form directly — RFC 8252 prohibits embedded user-agents for OAuth precisely because a host-app-controlled `WebView` can read the login form's credentials as they're typed; Custom Tabs give the requested "stays in the app" feel without that risk, since the actual system browser (with its own isolated process, cookies, and saved passwords) still renders the page. No existing OAuth protocol code (`OAuthAuthorizationRequest`, `OAuthCallbackValidator`, `Pkce`, `OAuthClientConfig`, the manifest's redirect-URI intent filter) needed any change — Custom Tabs resolve the same `ACTION_VIEW` redirect a full external browser would.
- Files changed: `MainActivity.kt`, `app/build.gradle.kts`, `gradle/libs.versions.toml`, `tasks/026-oauth-custom-tabs.md`.
- Checks and results: `./gradlew compileDebugKotlin` BUILD SUCCESSFUL; `./gradlew assembleDebug testDebugUnitTest lintDebug` BUILD SUCCESSFUL in 3m 28s, 53 actionable tasks, all unit tests passing, 0 lint errors. Installed on the human product owner's device over wireless `adb` after re-pairing.
- Review findings and resolutions: None from a separate review pass — the WebView-vs-Custom-Tabs security tradeoff was reasoned through directly against RFC 8252 rather than found as a defect afterward.
- Known limitations: The Custom Tab's toolbar isn't tinted to match the app's own color scheme (left plain rather than hardcoding a color inconsistent with the app's dynamic/Material You theming) — a minor visual polish item, not a functional gap. The OAuth consent ("Authorize") click itself is unchanged and unavoidable by design; only the surface it renders on changed. Live end-to-end confirmation of the login flow is pending as of this commit.
- Next recommended task: Get the human product owner's live confirmation of the login flow, then continue to the two remaining queued items from TASK-024/025 (image viewer, Issues/PR numbered pagination).
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-025 follow-up: fix diff-view horizontal-scroll direction
- Agent and role: Claude, self-directed device testing — the human product owner re-paired their device over wireless `adb` under a fresh pairing code/port and asked to test horizontal scroll on the current screen directly, rather than reporting a specific symptom first.
- Commit/diff: continued on `claude/task-015-notifications`; `feature/history/HistoryScreen.kt` only.
- Bug: verified by scripting `adb shell input swipe` in both directions against the live device and comparing before/after screenshots (rather than waiting for the human product owner to notice and describe it): the diff view's horizontal scroll, jitter-free since the previous follow-up, ran backwards — dragging left revealed the very start of the line (the `-`/`+ ` prefix) instead of further content along it, and dragging right did the opposite. Every other horizontally-scrollable surface on Android does the reverse (drag left reveals more content to the right, i.e. "forward").
- Root cause: the manually-driven scroll offset from the previous follow-up computed `newOffset = horizontalOffsetPx + delta` inside `rememberScrollableState { delta -> ... }`; the sign was backwards for how Compose's `scrollable` reports `delta` for this gesture.
- Fix: flipped to `newOffset = horizontalOffsetPx - delta` (with the returned consumed-delta value adjusted to match). Reverified with the same scripted-swipe-plus-screenshot method: dragging left now reveals later content, dragging right returns toward the start, and both diff panels stay in sync throughout.
- Files changed: `feature/history/HistoryScreen.kt`, `tasks/025-branch-detail-tabs.md` (evidence updated in place, same as the prior diff-scroll follow-up).
- Checks and results: `./gradlew assembleDebug testDebugUnitTest lintDebug` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors. Reinstalled on the human product owner's device over wireless `adb`; the direction fix itself was verified directly via scripted swipes rather than waiting on a live report.
- Review findings and resolutions: None from a separate review pass — found via direct device testing prompted by the human product owner's "좌우 스크롤 해서 이상한걸 감지해볼래?" request, rather than a bug report.
- Known limitations: While investigating, also confirmed that partial characters get visually clipped at the viewport edge when scrolled to a non-character-aligned pixel position — this is expected behavior for any horizontally-scrolled monospace text (the same happens in GitHub's and VS Code's own diff/code viewers) and was not treated as a defect, though it was more noticeable while the direction bug was still live and scroll landed at unusual positions.
- Next recommended task: Get the human product owner's live confirmation of the diff view (jitter-free and now correctly directional) alongside the rest of the TASK-025 batch and the TASK-026 Custom Tabs login flow, then continue to the two remaining queued items (image viewer, Issues/PR numbered pagination).
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-025 follow-up: fix tab-swipe vs. code-scroll gesture conflict
- Agent and role: Claude, live device testing continued directly from the direction-fix follow-up above; this time the human product owner watched the same swipe testing and reported "이거 왤케 떨리냐 화면이...?" (why is the screen trembling so much), then, after Claude's own scripted-swipe screenshots didn't reproduce anything conclusive, correctly root-caused it themselves: "저거 히스토리랑 파일 좌우 탭전환 기능이랑 코드 좌우 스크롤 기능이 섞이면서 그런 것 같은데" (the History/Files tab-swipe and the code's own left/right scroll are colliding).
- Commit/diff: continued on `claude/task-015-notifications`; `feature/branchdetail/BranchDetailScreen.kt` only.
- Bug: `BranchDetailScreen`'s `HorizontalPager` (Files/History tab-swipe) and the diff view's own horizontal code scroll (from the two prior follow-ups) are both horizontal gestures layered on the same screen. Nested, they both tried to consume the same drag — visible as jittery/trembling scrolling, and separately (per a second report in the same message) as source text sometimes scrolling out of view entirely while viewing code.
- Fix: the human product owner specified the exact intended boundary directly — disable tab-swiping only while actual source code is on screen (a file's contents on the Files tab, or a selected file's diff on the History tab), keep it enabled everywhere else (folder listings, the commit list, a commit's changed-file list). Implemented as `HorizontalPager(userScrollEnabled = !currentPageShowsScrollableCode, ...)`, where `currentPageShowsScrollableCode` reads the already-available `codeBrowserState`/`historyState` (both were already being collected in `MainActivity` and passed into this screen, so no new state plumbing was needed) depending on which page is current. Tapping a tab directly still always works, since that path is `animateScrollToPage`, not a gesture.
- Files changed: `feature/branchdetail/BranchDetailScreen.kt`, `tasks/025-branch-detail-tabs.md` (evidence updated in place, same pattern as the two prior diff-scroll follow-ups).
- Checks and results: `./gradlew assembleDebug testDebugUnitTest lintDebug` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors. Reinstalled on the human product owner's device over wireless `adb`; verified directly via scripted `adb shell input swipe` in both directions on both the code view (tab-swipe correctly disabled, code scrolls cleanly) and the file-list view (tab-swipe correctly still works) — an initial version of this same verification looked like a regression (swiping didn't switch tabs on the list view either) until re-checked: that test had swiped in the direction of a page that doesn't exist (already at the last page), not evidence of a real bug.
- Review findings and resolutions: None from a separate review pass. Notably, the human product owner diagnosed the actual root cause directly from watching the live behavior, ahead of Claude's own investigation — Claude's scripted-swipe screenshot sampling (∼1s intervals) had already been established as too coarse to catch genuine per-frame jitter, so the human's direct observation was the deciding signal here, not device automation.
- Known limitations: Whether the trembling is now fully gone (as opposed to just less likely to occur) has not been separately re-confirmed live by the human product owner as of this commit — the scripted-swipe verification confirms the gesture-ownership conflict is structurally resolved (only one consumer is ever active at a time now), but a live human touch test is the only way to confirm the felt result.
- Next recommended task: Get the human product owner's live confirmation of the whole TASK-025 batch plus this fix and the TASK-026 Custom Tabs login flow, together in one pass, then continue to the two remaining queued items (image viewer, Issues/PR numbered pagination).
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-027: image viewer with swipe gallery navigation, and CSV table viewer
- Agent and role: Claude, implementing two features requested together in one message: an image viewer (asked for immediately after the swipe-conflict fix above shipped) and, mid-implementation, a CSV-as-table viewer (asked for as a follow-on message while this task was already in progress).
- Commit/diff: continued on `claude/task-015-notifications`; `feature/codebrowser/content/ContentClassification.kt`, `feature/codebrowser/content/SourceContentClassifier.kt`, `feature/codebrowser/CodeBrowserViewModel.kt`, `feature/codebrowser/CodeBrowserScreen.kt`, `feature/branchdetail/BranchDetailScreen.kt`, `MainActivity.kt`, plus matching tests and `tasks/027-image-and-csv-viewer.md`.
- Features implemented: opening an image file (PNG/JPEG/GIF/BMP/WebP, detected by magic bytes not extension) now decodes and shows it, with left/right swipe moving to the previous/next image among the same folder's other images — explicitly required to never trigger the Files/History tab underneath it to change, per the human product owner's direct instruction ("탭 전환은 발생하면 안돼"). Opening a `.csv` file now shows a scrollable table (rows/columns, first row styled as a header) instead of raw comma-separated text.
- How it was implemented: image detection runs before the pre-existing NUL-byte-means-Binary check in `SourceContentClassifier`, since every one of these formats' bytes contain NULs and would otherwise always be caught as generic Binary first. The sibling-image gallery is built for free from the directory listing already fetched to reach the file (`CodeBrowserViewModel.openEntry` captures the current `Directory` state's image-extension file entries into the new `File` state's `siblingImagePaths`), rather than a second network round-trip. `navigateImage(delta)` looks up the current path's index in that list and loads the neighbor. The image swipe itself is a discrete drag-distance threshold (not a continuous scroll position, unlike the diff view), while the CSV table reuses the diff view's manually-driven shared-offset horizontal-scroll pattern verbatim for consistency, even though CSV's fixed-width cells mean the specific bound-thrashing bug that pattern exists to avoid couldn't actually occur here. The tab-swipe-vs-content-scroll gating fix from the previous follow-up was widened (not duplicated) to also cover Image and Csv content.
- Files changed: see `tasks/027-image-and-csv-viewer.md`'s Scope section for the full list.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin` BUILD SUCCESSFUL; `./gradlew testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (new coverage: magic-byte detection for all five formats, an image signature's embedded NUL not short-circuiting to Binary, oversized-before-image-detection ordering, CSV path-extension gating, quoted-field/escaped-quote parsing, row-truncation, gallery capture in listing order excluding a non-image sibling, `navigateImage` moving and stopping at both ends, `navigateImage` no-op outside a directory listing); `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: Not yet confirmed against a real image or CSV file on the live device — every repository browsed this session for a suitable test file (see TASK-028's log entry below for the same search, done for the same reason) didn't have one readily reachable. Relying on the unit test coverage above until a live file turns up. CSV column width is a fixed 120dp per cell, not sized to content.
- Next recommended task: Get live confirmation of both this and TASK-028 (or supply/point to a repo with an actual image/CSV file and some issues/PRs), otherwise continue per the standing direction.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-028: numbered, jump-to-page pagination for the Issues/PR list
- Agent and role: Claude, implementing the second half of the same combined request as TASK-027 above.
- Commit/diff: continued on `claude/task-015-notifications`; `domain/model/RepositoryModels.kt`, `data/workitems/GiteaWorkItemsApi.kt`, `feature/workitems/WorkItemsViewModel.kt`, `feature/workitems/WorkItemsScreen.kt`, `MainActivity.kt`, plus matching tests and `tasks/028-issues-prs-numbered-pagination.md`.
- Feature implemented: the Issues list and the PR list each now show up to 5 numbered page buttons (tap to jump directly), previous/next, and a `[ page / total ]`-style text field to type an exact page number and jump straight there — replacing plain previous/next-only, on this screen alone. Matches the earlier AskUserQuestion resolution from this session (apply numbered pagination only where a total count is knowable) — repository/history/notification lists are unchanged, still plain previous/next, since Gitea's endpoints for those don't report a total.
- How it was implemented: verified directly against Gitea's own source (`routers/api/v1/repo/issue.go`'s swagger doc comment) that both the issue-list and pull-request-list endpoints report a total match count via `X-Total-Count`, and that a documented `type=issues`/`type=pulls` query parameter exists — rather than assume either. `listIssues` now sends `type=issues` so both the returned items and the total count exclude pull requests server-side (previously, filtering was client-side only, which the class doc comment had explicitly defended as deliberate — that reasoning was rewritten now that a verified server-side option exists). `Page<T>` gained an optional, defaulted `totalCount`, so none of the app's other 9+ `Page.of(...)` call sites needed to change. The numbered footer falls back to the original plain previous/next row whenever `totalPages` is null, rather than ever showing a broken or missing count.
- Files changed: see `tasks/028-issues-prs-numbered-pagination.md`'s Scope section for the full list.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin` BUILD SUCCESSFUL; `./gradlew testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (new coverage: `type=issues` sent, `X-Total-Count` parsed for both issues and pull requests, absent header leaves `totalCount` null, a fractional total rounds up to the correct page count, `goToPage` jumps directly, `Page.totalCount` defaults/validates correctly); `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: Checked the Issues and Pull Request tabs on four of the human product owner's actual repositories live; every one had zero issues and zero pull requests, so the numbered-pagination UI was never actually populated with real data this session — confirmed instead that the screen doesn't crash and correctly falls back to the existing empty state on all four. Relying on the unit test coverage above (header parsing, total-page-count math, jump behavior) until a repository with real issues/PRs is available to confirm the rendered UI itself.
- Next recommended task: Get live confirmation of both this and TASK-027 (or supply/point to a repository with real issues/PRs and an image/CSV file), otherwise continue per the standing direction — no other items remain on the queued list from this session.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-027 follow-up: pinch-to-zoom and pan for the image viewer
- Agent and role: Claude, direct continuation of the same live session — the human product owner confirmed the image opened correctly, then asked for zoom directly ("확대 축소 기능도 만들어줄래?").
- Commit/diff: continued on `claude/task-015-notifications`; `feature/codebrowser/CodeBrowserScreen.kt` only.
- Feature implemented: pinch-to-zoom (1x–5x) with pan while zoomed in, and double-tap to toggle between 1x and 2x — added to the image viewer from the earlier TASK-027 commit.
- How it was implemented: rather than stacking a separate zoom/pan gesture detector alongside the existing swipe-navigate one (which would have reintroduced exactly the class of bug TASK-025 hit twice — two independent `pointerInput` detectors both trying to consume the same single-finger drag), all three behaviors — pinch-zoom, pan-while-zoomed, and swipe-navigate — were combined into one hand-rolled `awaitEachGesture` loop that reads live pointer count each event and branches: 2+ pointers is always pinch-zoom/pan (`calculateZoom`/`calculatePan`); 1 pointer while already zoomed in pans; 1 pointer at 1x is reserved for the pre-existing swipe-navigate drag accumulator. A double-tap toggle was added via a second, non-competing `pointerInput` (`detectTapGestures` doesn't accumulate drag distance, so it doesn't fight the first block). Zoom/pan state is `remember(content)`-scoped so navigating to a different image in the gallery always starts that image at 1x rather than carrying over the previous image's zoom. Pan is clamped to an approximate (not pixel-exact) bound to keep a zoomed image roughly on screen.
- Files changed: `feature/codebrowser/CodeBrowserScreen.kt`, `tasks/027-image-and-csv-viewer.md` (evidence updated in place, matching the pattern used for TASK-025's own follow-ups).
- Checks and results: `./gradlew compileDebugKotlin` BUILD SUCCESSFUL; `./gradlew assembleDebug testDebugUnitTest lintDebug` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors. Reinstalled on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass — the single-gesture-loop design was chosen proactively, reasoning directly from the two prior tab-swipe/content-scroll gesture conflicts this session had already found and fixed, rather than discovered as a defect afterward.
- Known limitations: The pinch-zoom/pan/double-tap gestures themselves haven't been live-confirmed yet (only that the image itself opens, from the prior install) — no dedicated unit test exists for this either, since it's pure Compose gesture-handling with no non-UI logic to isolate. The single-gesture-loop design structurally prevents the earlier bug class by construction, but that's a design argument pending an actual live touch test, not a substitute for one.
- Next recommended task: Get live confirmation of the zoom/pan/double-tap gestures specifically, plus the still-outstanding CSV table and Issues/PR numbered pagination confirmations from earlier today, otherwise continue per the standing direction.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-027 follow-up: PDF viewer
- Agent and role: Claude, direct continuation of the same live session — the human product owner tried opening a PDF file next and asked "pdf 파일은 못읽나?" (can't PDF files be read?).
- Commit/diff: continued on `claude/task-015-notifications`; `feature/codebrowser/content/ContentClassification.kt`, `feature/codebrowser/content/SourceContentClassifier.kt`, `feature/codebrowser/CodeBrowserViewModel.kt`, `feature/codebrowser/CodeBrowserScreen.kt`, `feature/branchdetail/BranchDetailScreen.kt`, plus a matching test and `tasks/027-image-and-csv-viewer.md`.
- Feature implemented: opening a `.pdf` file (detected by `%PDF-` magic bytes) now renders its pages, with the same left/right swipe-between-pages, pinch-zoom, pan, and double-tap-to-toggle-zoom interaction already built for the image viewer.
- How it was implemented: rendering uses Android's built-in `android.graphics.pdf.PdfRenderer` (API 21+, no compatibility concern against this app's `minSdk = 28`) — no new dependency, matching how the image viewer already used the platform's own `BitmapFactory` instead of a third-party decoding library. `PdfRenderer` only accepts a `ParcelFileDescriptor` backed by a real file, so the already-fetched bytes get written to a throwaway temp file purely to satisfy that, deleted in a `finally` block regardless of outcome; the file is fully re-opened and the requested page re-rendered on every page turn rather than keeping a `PdfRenderer` held open across recompositions, trading some redundant I/O for avoiding a class of lifecycle bug that's easy to introduce holding a stateful Android resource across Compose's recomposition lifecycle. The pinch-zoom/pan/swipe-navigate/double-tap gesture code from the prior pinch-zoom follow-up was extracted into a shared `ZoomableSwipeableBox` composable specifically so this viewer could reuse it verbatim rather than duplicating that fairly intricate gesture-arbitration logic a second time. Classification itself only detects "is this a PDF" by magic bytes — page count and rendering stay entirely in the UI layer (`LaunchedEffect`), the same split already used for images, keeping the classifier framework-agnostic and unit-testable without Robolectric. Separately, `maxInputBytes` was raised from 1MB to 10MB in `CodeBrowserViewModel`'s classifier config — at 1MB, most real-world photos and PDFs would have been rejected as Oversized before ever being classified, which would have made both this feature and the image viewer close to useless in practice; 10MB is still a deliberately conservative mobile-safe ceiling, not "however large."
- Files changed: see `tasks/027-image-and-csv-viewer.md`'s Scope section for the full list.
- Checks and results: `./gradlew compileDebugKotlin` BUILD SUCCESSFUL; `./gradlew compileDebugUnitTestKotlin testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (new coverage: `%PDF-` signature detection, its embedded NUL bytes not short-circuiting to Binary, oversized-before-PDF-detection ordering, plain text starting with `%` not misclassified as PDF); `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: Neither the PDF rendering path nor the shared gesture code has a dedicated unit test — both are Android-framework/pure-UI concerns with no non-UI logic to isolate (`PdfRenderer` specifically isn't available in a plain JVM unit test without Robolectric). Not yet confirmed against a real `.pdf` file on the live device — no repository browsed this session had one readily reachable, same as the still-outstanding CSV table confirmation. Re-opening and fully re-rendering the file on every page turn hasn't been performance-checked against a real large, many-page PDF.
- Next recommended task: Get live confirmation of the PDF viewer, the CSV table, pinch-zoom/pan/double-tap, and the Issues/PR numbered pagination — everything built this session that's still pending a live look — otherwise continue per the standing direction. No other items remain queued.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-029: OAuth refresh-token support
- Agent and role: Claude, responding directly to a live complaint — "그리고 애플리케이션 승인 안하고 로그인 쭉 이어서 되어있게 해주면 안되나?" (can't login just stay continuous without having to approve the application) — asked immediately after a separate feature request (push notifications for favorited repos, see feasibility note below) revealed *why* it mattered to the human product owner: reliable long-lived auth is a prerequisite for any kind of background checking.
- Commit/diff: continued on `claude/task-015-notifications`; `data/auth/AuthApi.kt`, new `core/auth/android/RefreshingTokenProvider.kt` and `core/auth/android/AuthSession.kt`, `feature/repositories/RepositoryListViewModel.kt`, and the `Factory` classes of all five ViewModels, plus matching tests and `tasks/029-oauth-token-refresh.md`.
- Bug found while investigating: the app already captured Gitea's OAuth refresh token on login (it was part of the persisted `TokenSet`) but never actually used it anywhere — `RepositoryListViewModel.restoreSession()` cleared the whole session and forced a brand-new browser-based login (Custom Tab open, Gitea login, "Authorize this application?" consent screen) the instant the access token's stated expiry passed, every single time, discarding a perfectly usable refresh token each time.
- How it was implemented: verified Gitea's actual `refresh_token` grant shape directly against its source (`routers/web/auth/oauth2_provider.go`'s `handleRefreshToken`) before implementing, per this project's established convention, rather than assuming the standard OAuth2 shape applied unmodified. Added `AuthenticationGateway.refreshToken` (shares the existing `exchangeCode` response-parsing code) and a new `RefreshingTokenProvider` that proactively refreshes a token 60 seconds ahead of its stated expiry (not reactively after a failed call) — proactive specifically because the provider is now *shared* across all five features' independent API clients via a new `AuthSession` singleton, replacing what used to be five separately-constructed, mutually-unaware `AndroidKeystoreTokenVault`/`BearerTokenProvider` pairs; sharing means a refresh triggered by one feature's next call is immediately visible to every other feature too, and the provider's internal `Mutex` actually prevents concurrent refreshes only because every feature now goes through the same instance. `RepositoryListViewModel.restoreSession()`'s old "already past its stated expiry → clear and fail immediately" clock check was removed outright (not adjusted) since it fired *before* the now-refresh-capable API call ever got a chance to run — the exact same fallback behavior (clear + `AUTHENTICATION_EXPIRED`) still happens today, just driven by a real failed API call instead of a client-side guess made too early.
- Files changed: see `tasks/029-oauth-token-refresh.md`'s Scope section for the full list.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin` BUILD SUCCESSFUL (after finding and fixing one missed `loadRepositories(tokens)` call site broken by the signature change); `./gradlew testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (after finding and fixing two bugs in the new tests' own fixture data — not the implementation — flagged by two initially-failing assertions); `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: Proactive-refresh only — no "catch a live 401 and retry once" fallback at the transport layer (a token revoked server-side for a reason other than ordinary expiry still surfaces as a normal 401, correctly, just not silently retried). Not yet confirmed against a real long-idle session on the device — the fix targets a problem that only manifests after enough real time passes for a token to actually expire, and this session's live testing so far has been continuous active use rather than a multi-hour gap.
- Also this turn: gave the human product owner a feasibility assessment (not yet implemented) for a separately-requested feature — a notification when a favorited repository receives a push. Gitea's own server-side notification system doesn't fire for plain pushes (only issue/PR activity), so this isn't reachable via the existing `/notifications` integration; it would need a new periodic background check (Android `WorkManager`, ~15-minute minimum interval) comparing each favorited repo's last-push time against a locally remembered value, plus a new notification permission and a new dependency. Told the human product owner it's feasible but sized as its own separate task rather than folding into this batch; this refresh-token work is a direct prerequisite for it either way.
- Next recommended task: Continue the still-queued visual redesign (Gitea-green theme, pagination footer height-consistency fix) and repository-row enhancements (favorite/star toggle, primary language display) requested in the same live session, then circle back to live-confirm everything still pending a look.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-030: Gitea-green theme, pagination footer height fix, repository row language/favorite
- Agent and role: Claude, direct continuation of the same live session — the human product owner, after a status-summary check-in and approving the remaining queued image-viewer/pagination work, sent a screenshot of the real Gitea web UI and asked for a matching green theme ("Gitea 자체가 녹색의 찻잎 같은 분위기니까 그거에 맞게 디자인을 진행해줄 수 있어?"), with an explicit hard constraint that functionality and interaction feel must never change, plus a separately-reported bug: the pagination footer's button heights kept visibly changing. A mid-turn message in the same batch also asked for each repository row to show its favorite/star action and primary language.
- Commit/diff: continued on `claude/task-015-notifications`; new `ui/theme/Color.kt`, rewritten `ui/theme/Theme.kt`, new `ui/PaginationFooter.kt`, `feature/history/HistoryScreen.kt`, `feature/notifications/NotificationsScreen.kt`, `feature/repositories/RepositoryListScreen.kt`, `feature/workitems/WorkItemsScreen.kt`, `domain/model/RepositoryModels.kt`, `data/repositories/GiteaRepositoryBrowserApi.kt`, `feature/repositories/RepositoryListViewModel.kt`, `MainActivity.kt`, a new/updated test, and `tasks/030-gitea-green-theme-and-repo-row-enhancements.md`.
- Feature implemented: the whole app now uses a real Material3 color scheme built from Gitea's own brand green instead of Compose's unthemed default purple (the app's `lightColorScheme()`/`darkColorScheme()` had never been given explicit role values before now); every pagination footer's buttons now render at one guaranteed-consistent height; and each repository row now shows its Gitea-reported primary language plus a tappable star toggle wired to the favorite action.
- How it was implemented: verified Gitea's actual brand green (`--color-logo: #609926`) against Gitea's own source before picking a palette, per this project's established convention, then hand-built a full light+dark tonal palette from that hue — `primary` itself uses a deliberately deepened `#3B6A1F` rather than the raw logo color, since white text on `#609926` sits under the WCAG 4.5:1 contrast threshold for buttons; `error`/diff-removed-line red was left untouched at Material3's baseline, since nothing about the request was about error coloring. The footer-height bug was fixed by extracting one shared `SimplePaginationFooter` used by all four screens' plain previous/next footers, plus pinning an explicit `Modifier.height(PaginationButtonHeight)` on every button in the work-items numbered footer (which had been mixing `TextButton`/`OutlinedButton` variants with an inconsistent custom `contentPadding` on the numbered buttons specifically) — an explicit height guarantees consistency by construction regardless of exactly which Material3 default/padding interaction was the literal cause. Language and star count needed no new network call, since Gitea's existing repository-list response already includes both fields (verified against Gitea's `Repository` struct); the star toggle reuses TASK-018's existing `toggleFavorite` action unmodified, with a new mode-aware `FavoriteStar` composable branching its rendering on whether the current list is ALL (tap stars) or FAVORITES (tap un-favorites and removes the row).
- Files changed: see `tasks/030-gitea-green-theme-and-repo-row-enhancements.md`'s Scope section for the full list.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin` BUILD SUCCESSFUL; `./gradlew testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (new coverage: repository language/star-count mapping, blank language becoming `null`); `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`; screenshots taken directly on the device confirm the green theme applied consistently, matching footer button heights, and language/star display on repository rows.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: a repository already starred before the current app session still shows an outlined (not filled) star until acted on this session — carried over unchanged from TASK-018's original design. No per-language color coding (no reliable local source for GitHub/Gitea's `linguist` color mapping) — language is shown as plain text. The hand-built palette's contrast was reasoned through directly for the primary/onPrimary pairing specifically, not verified with an automated contrast checker across every color role. Full live human confirmation of this batch is still pending, even though device screenshots taken this session already show all three pieces working correctly.
- Next recommended task: Get live confirmation from the human product owner of everything still pending a look this session — pinch-zoom/pan on images, the CSV table viewer, the PDF viewer, Issues/PR numbered pagination, and now this theme/footer/star-language batch — then, if wanted, size and build the previously-scoped push-notification-on-favorited-repo-push feature as its own task.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-031: "커밋" label rename, EUC-KR decode fallback, HTTP timeout/connection-pooling fix
- Agent and role: Claude, direct continuation of the same live session. Three separate live bug reports arrived while TASK-030 was being finished up: after confirming the "History" tab/screen was in fact the commit history, the human product owner asked for the label itself to say "커밋" instead; separately, "지금 .cpp, .h 파일은 잘 못읽는 것 같다 자꾸 UTF-8로 안전하게 읽을 수 없는 파일이래" (`.cpp`/`.h` files keep failing to open, saying they can't be safely read as UTF-8) plus "파일 양이 많아지면 불러오는데 너무 오래 걸려" (loading takes too long once there are a lot of files); and, while investigating those, a further live report — "LGD-P9-CLCI-4K는 커밋 이력을 보려고 하면 서버에 연결할 수 없습니다... 이거 이전에도 비슷한거 그랬었지 않았나?" (that repository's commit history says it can't reach the server — didn't something similar happen before?) — which traced back to the same underlying cause as the "too slow" complaint.
- Commit/diff: continued on `claude/task-015-notifications`; `strings_branchdetail.xml`, `strings_codebrowser.xml`, `strings_history.xml`, `feature/codebrowser/content/SourceContentClassifier.kt` (+ test), `core/network/HttpTransport.kt`, plus `tasks/031-commit-label-euckr-fallback-http-timeouts.md`.
- Feature/fixes implemented: (1) every user-facing "히스토리" label became "커밋" (a full `res/values` scan found exactly three occurrences: the branch-detail tab, the code-browser's history button, and one sentence in a permission-denied message); (2) the content classifier now tries EUC-KR (strictly, same as its existing UTF-8 attempt) before giving up, so legacy Korean-encoded C/C++ source files render instead of failing as `UnsupportedEncoding`; (3) `OkHttpTransport`'s default client changed from a fresh, unconfigured `OkHttpClient()` per call site to one shared client with longer timeouts (15s connect / 30s read / 15s write, versus the previous unconfigured 10s/10s/10s).
- Bug found while investigating: the "server unreachable" error on a large repository's commit history and the "loading takes too long" complaint traced back to the exact same root cause — every one of the app's ~8 `OkHttpTransport()` call sites built its own fresh, default-configured `OkHttpClient()`, whose 10-second timeouts were too tight for this app's actual Gitea host (a home Synology NAS, not a datacenter service); `GiteaRepositoryBrowserApi.authenticated`/`GiteaHistoryApi.get` both catch `IOException` broadly (which includes `SocketTimeoutException`) and wrap it as `ApiFailure.Network`, rendered identically to a genuinely offline device — a slow-but-working request on a large commit history or a directory with many entries was being cut off and misreported before it had a real chance to finish.
- How it was implemented: the EUC-KR fallback reuses the classifier's existing strict-decode discipline (`CodingErrorAction.REPORT`, never `REPLACE`) so a second well-defined charset is tried, not a permissive catch-all — a byte sequence invalid in both UTF-8 and EUC-KR still correctly falls through to `UnsupportedEncoding`. EUC-KR specifically, not the larger MS949/CP949 superset, because Android's own `Charset` documentation guarantees EUC-KR is bundled on every device via ICU while CP949 support isn't guaranteed — using CP949 risked passing this project's host-JVM unit tests while throwing `UnsupportedCharsetException` at runtime on the actual device. The HTTP fix is one shared, lazily-built `OkHttpClient` used as `OkHttpTransport`'s new default, which both gives slow-but-working requests room to complete and lets every feature's calls to the same host reuse one connection pool (avoiding a fresh TCP/TLS handshake per feature) instead of each of the ~8 call sites opening its own.
- Files changed: see `tasks/031-commit-label-euckr-fallback-http-timeouts.md`'s Scope section for the full list.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin` BUILD SUCCESSFUL; `./gradlew testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (new coverage: EUC-KR-encoded text classifies as `Text`; a byte sequence invalid in both UTF-8 and EUC-KR still classifies as `UnsupportedEncoding`; the two pre-existing "malformed UTF-8" tests were checked against EUC-KR's own lead/trail-byte rules to confirm they still exercise the "both attempts fail" path). `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: the error message still doesn't distinguish "genuinely offline" from "reachable but slow" — a request still too slow even at 30 seconds, or a truly offline device, both still render the same message; the fix targets the common slow-NAS case specifically, not a full error-taxonomy redesign. EUC-KR only, not CP949's extended completion range (deliberate, see task file). No response caching was added — the connection-pool sharing cuts handshake overhead, not the request/response itself.
- Next recommended task: get live confirmation from the human product owner that "LGD-P9-CLCI-4K"'s commit history now loads, that `.cpp`/`.h` files open correctly, and that the "커밋" label reads right — not yet re-tested against those exact cases in this session. Also still pending from earlier in this session: the green theme/footer-height/language-star batch (TASK-030), pinch-zoom/pan, CSV, PDF, and Issues/PR numbered pagination.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-032: fix reversed ALL/FAVORITES swipe direction on the repository list
- Agent and role: Claude, direct continuation of the same live session — "메인 화면 좌우 탭전환 스와이프가 반대로 되어 있는 것 같다" (the main screen's left/right tab-switch swipe seems reversed).
- Commit/diff: continued on `claude/task-015-notifications`; `feature/repositories/RepositoryListScreen.kt` only, plus `tasks/032-repository-list-swipe-direction-fix.md`.
- Bug found: the repository list (the app's actual home screen after login, distinct from `BranchDetailScreen`'s already-correct Files/Commit `HorizontalPager`) switches its ALL/FAVORITES tabs via a hand-rolled `detectHorizontalDragGestures` modifier rather than `HorizontalPager`, and that gesture's `onDragEnd` had its comparison signs backwards relative to the drag-follows-finger convention every other swipe surface in this app already uses: swiping right moved ALL -> FAVORITES and swiping left moved FAVORITES -> ALL, the opposite of what a rightward drag revealing the left-hand tab (and vice versa) should do.
- How it was implemented: the two `when` branch conditions in `modeSwipeGesture`'s `onDragEnd` were swapped (comparison direction only — the drag-accumulation and threshold logic were untouched), confirmed correct by re-deriving the convention directly from `BranchDetailScreen`'s already-correct `HorizontalPager` behavior and from `ModeTabs`'s actual tab order (ALL left, FAVORITES right) rather than guessing.
- Files changed: `feature/repositories/RepositoryListScreen.kt`, `tasks/032-repository-list-swipe-direction-fix.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin` BUILD SUCCESSFUL; `./gradlew testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (no dedicated test for this gesture, same category as this app's other pure-Compose gesture code); `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: no dedicated unit test for the gesture direction itself; live confirmation of the corrected swipe feel is still pending in this session.
- Also this turn: while looking into a separately-reported bug (tapping a repository's new favorite star turns it red but doesn't actually favorite it), traced the root cause to a real architectural conflict rather than a simple bug — Gitea's `PUT`/`DELETE /user/starred/{owner}/{repo}` endpoints require `write:repository` OAuth scope (verified against Gitea's own route registration source), but this app's `OAuthClientConfig` has a hard `require(scopes.all { it.isReadOnly })` invariant and only ever requests read-only scopes. Every star/unstar attempt is therefore guaranteed to fail server-side (403), caught generically, and rendered as the reported red "failed" star. Not fixed yet — this needs the human product owner's direction, since the only real fix (requesting `write:repository`) breaks the just-built persistent-login promise from TASK-029 at least once (a scope change requires fresh consent) and changes this app's deliberate read-only security posture; flagged to them rather than decided unilaterally.
- Next recommended task: get the human product owner's direction on the star/favorite OAuth-scope conflict (add write scope vs. make favorites a local-only bookmark vs. remove the feature), then continue per the standing direction — still pending from earlier in this session: TASK-030's green theme/footer-height/language-star batch, TASK-031's label/EUC-KR/timeout batch, pinch-zoom/pan, CSV, PDF, and Issues/PR numbered pagination, all awaiting live confirmation.
- Next owner: Claude, continuing per the standing direction once the human product owner responds on the OAuth-scope question.
## 2026-08-27 — TASK-033: request write:repository OAuth scope so starring actually works
- Agent and role: Claude, direct continuation of the same live session — following up on the star/unstar-always-fails root cause traced during TASK-032. Presented the human product owner with three options (add write:repository scope / make favorites a local-only bookmark / remove the feature); they chose to add the scope.
- Commit/diff: continued on `claude/task-015-notifications`; `core/auth/protocol/OAuthClientConfig.kt` and `OAuthClientConfigTest.kt`, plus `tasks/033-write-repository-oauth-scope-for-starring.md`.
- Feature implemented: `write:repository` is now requested alongside this app's existing read-only OAuth scopes, added as one narrow, explicit exception to the `require(scopes.all { it.isReadOnly })` invariant rather than a blanket relaxation — every other write scope (including the pre-existing, still-unused `WRITE_USER` value) is still rejected, confirmed by a new test that pairs it with the now-allowed `write:repository` and checks it's still rejected.
- How it was implemented: verified directly against Gitea's own route registration source (`routers/api/v1/api.go`) that `PUT`/`DELETE /user/starred/{owner}/{repo}` require Write-level access within the repository scope category — `read:repository` alone can never satisfy it, so this wasn't fixable from the client side any other way. No change was needed to `toggleFavorite`, `FavoriteStar`, or any other favorite-feature code from TASK-018/TASK-030 — all of that was already correctly built against the `RepositoryFavorites` interface; the only thing wrong was the token it was being handed never had permission to actually star anything.
- Files changed: `core/auth/protocol/OAuthClientConfig.kt`, `OAuthClientConfigTest.kt`, `tasks/033-write-repository-oauth-scope-for-starring.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin` BUILD SUCCESSFUL; `./gradlew testDebugUnitTest` BUILD SUCCESSFUL, all unit tests passing (new coverage: `write:repository` alone is now accepted; `write:user` is still rejected even alongside the now-allowed `write:repository`; the pre-existing write-scope-rejection test is unaffected); `./gradlew assembleDebug lintDebug` BUILD SUCCESSFUL, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: a device already logged in under the old, read-only-only scope needs one explicit re-login before starring actually works — TASK-029's refresh-token flow keeps a session alive indefinitely for the scope it was originally granted, and a refresh doesn't expand that scope. This is expected and was reported directly to the human product owner rather than silently worked around (e.g. by force-clearing the stored session on their behalf), since that kind of forced logout should be their call. Live confirmation (star actually succeeding after a fresh login) is still pending.
- Next recommended task: ask the human product owner to log out and back in once, then confirm starring/unstarring actually works. Still pending from earlier in this session: TASK-030's green theme/footer-height/language-star batch, TASK-031's label/EUC-KR/timeout batch, TASK-032's swipe-direction fix, pinch-zoom/pan, CSV, PDF, and Issues/PR numbered pagination, all awaiting live confirmation.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-27 — TASK-033 follow-up: diagnosed the "denied:server_error" login failure after the scope change
- Agent and role: Claude, direct continuation of the same live session — "로그인이 안돼" (login isn't working) after installing the write:repository build; device screenshot showed "로그인 응답을 확인할 수 없습니다... [debug] denied:server_error".
- Diagnosis, not a code change: verified the running Gitea instance is 1.23.5 (via `/api/v1/version`, confirmed a modern version with full scoped-OAuth-token support, ruling out a version gap). Reproduced the authorize request's own URL directly (dummy PKCE values, real client_id/redirect_uri/scope) in a browser to confirm it reaches Gitea's login page cleanly rather than erroring at the HTTP-parsing level. Then traced `GrantApplicationOAuth` in Gitea's own source (`routers/web/auth/oauth2_provider.go`, 1.23 branch) and found the exact check responsible: `if grant.Scope != form.Scope { ...ErrorCode: ErrorCodeServerError, ErrorDescription: "a grant exists with different scope" }` — Gitea deliberately refuses to silently widen an already-existing OAuth2 grant's scope. The device's account already had a grant for this app from before TASK-033's scope change, stored under the old read-only-only scope string; the new, differently-scoped login request hits this check every time, which is exactly the "server_error" shown.
- Files changed: `tasks/033-write-repository-oauth-scope-for-starring.md` (Known limitations section) only — no source changed, since this is Gitea's own intended behavior, not a bug in the app.
- Fix (not yet done): the human product owner needs to revoke UBGitea's existing authorization from the Gitea web UI (Settings → Applications → "Authorized OAuth2 Applications" → revoke), then log in again on the device — this creates a fresh grant under the new scope and should show the consent screen listing `write:repository`.
- Next recommended task: wait for the human product owner to revoke the old grant and retry login, then confirm starring actually works. Everything else queued from earlier in this session remains pending live confirmation.
- Next owner: Claude, continuing per the standing direction once the human product owner responds.
## 2026-08-27 (late) — TASK-033 correction: starring needs write:user AND write:repository together
- Agent and role: Claude, direct continuation of the same live session — "그대론데..?" (it's still the same) after the human product owner re-tried with the write:user-only build.
- Diagnosis: added debug-build-only logging of the raw HTTP status + response body on any non-2xx response (`HttpTransport.requireSuccess`) plus the failure class/message in `toggleFavorite`'s catch block, since source-reading alone had already produced one wrong answer this session. Required adding `testOptions { unitTests { isReturnDefaultValues = true } }` to `app/build.gradle.kts` so those `android.util.Log` calls don't throw inside plain JVM unit tests. Live logcat then showed two direct, unambiguous server responses: requesting `write:repository` alone got `required=[write:user]`; requesting `write:user` alone then got `required=[write:repository]` right back. Together these confirm `/user/starred/{owner}/{repo}` requires **both** scopes at once, not either alone.
- Fix: `OAuthClientConfig` now requests both `write:user` and `write:repository`; the narrowing `require()` invariant allows exactly those two (confirmed still narrow by a new test using an unrelated write scope, `write:organization`, added purely for that test).
- Files changed: `OAuthClientConfig.kt`, `OAuthClientConfigTest.kt`, `HttpTransport.kt`, `RepositoryListViewModel.kt`, `app/build.gradle.kts`, `tasks/033-write-repository-oauth-scope-for-starring.md` (corrected throughout, including the earlier incorrect "verified against Gitea's source" claim).
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Known limitations: the device's current session still carries scope from an earlier attempt in this same investigation — needs one more revoke (Gitea Settings → Applications → Authorized OAuth2 Applications) and re-login before starring can be confirmed working. Not yet done — the human product owner was tired and this was left for a later session rather than pushed further tonight.
- Next recommended task: revoke + re-login when convenient, then confirm starring actually works; otherwise continue per the standing direction on everything else still queued from this session (TASK-030/031/032, pinch-zoom/pan, CSV, PDF, Issues/PR pagination).
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 — TASK-034: favorite star count doesn't increment after a successful star
- Agent and role: Claude, direct continuation of the same live session — the human product owner confirmed TASK-033's fix worked ("별표 눌러서 즐겨찾기는 되는데") but reported the count itself doesn't update ("카운팅은 안올라가네").
- Commit/diff: on `claude/task-015-notifications`; `feature/repositories/RepositoryListViewModel.kt` (+ test additions), `tasks/034-favorite-star-count-not-incrementing.md`.
- Bug found: `toggleFavorite`'s ALL-list success branch updated only `favoriteAction` to `Starred`, never `starsCount` — the row kept showing whatever count the last full list load had returned, even after a star the server had actually accepted.
- How it was implemented: incremented `starsCount` by 1 in the same state update that marks the row `Starred` (optimistic, since Gitea's star endpoint returns 204 with no body — a second network call just to re-fetch one number wasn't worth it). While fixing this, also closed a related gap: since the ALL list can't know a repository's already-starred state on initial load (an existing, accepted limitation from TASK-018/TASK-030), a `Starred` row's star stayed tappable, and a repeat tap would have re-called `star()` and double-incremented the displayed count each time — added an early-return guard so a repeat tap on an already-`Starred` row is now a no-op.
- Files changed: `RepositoryListViewModel.kt`, `RepositoryListViewModelTest.kt`, `tasks/034-favorite-star-count-not-incrementing.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing (new coverage: starring increments displayed count by 1; a repeat tap after a successful star is a no-op), 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Also this turn: the `task-015-notifications` worktree directory itself had been removed between sessions (its branch and commits were intact); recreated it with `git worktree add`, plus a fresh `local.properties` (gitignored, not tracked) pointing at the local Android SDK, before continuing.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: still optimistic, not authoritative — if Gitea's real count ever differs from this app's local +1 (e.g. a concurrent star from another client), the display stays off until the next full list reload. The underlying "can't tell if already starred on load" limitation is unchanged; this only stops it from causing a visible count bug.
- Next recommended task: get live confirmation that the count now increments on tap; otherwise continue per the standing direction on everything else still queued from this session.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-034 follow-up: repeat tap now un-stars instead of doing nothing
- Agent and role: Claude, direct continuation of the same live session — immediately after confirming the count now increments, the human product owner reported "이제 올라가는데 다시 눌렀을때 취소가 안되네" (it goes up now, but tapping again doesn't un-star it).
- Bug found: the first pass's fix for the double-count bug added an early-return guard that made a repeat tap on an already-`Starred` row a complete no-op — stopped the double-count, but also made the star impossible to undo from the ALL list without switching to the FAVORITES tab, which is not what a filled/tappable star should do.
- How it was implemented: replaced the no-op guard with a real toggle — `toggleFavorite` now branches on the row's own `favoriteAction`: not-yet-starred-this-session calls `star()` (`+1`, → `Starred`), already-starred-this-session calls `unstar()` (`-1`, coerced at 0, → `Idle`). Tests updated to match: the old "repeat tap is a no-op" test was replaced with one asserting the repeat tap actually un-stars (both `star()` and `unstar()` get called, count and state both revert), plus a new round-trip test (star then unstar lands back at the original count).
- Files changed: `RepositoryListViewModel.kt`, `RepositoryListViewModelTest.kt`, `tasks/034-favorite-star-count-not-incrementing.md` (updated throughout to describe both rounds of this fix).
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Also this turn: the `task-015-notifications` worktree had been removed from disk between sessions (branch/commits were intact in the main repo); recreated via `git worktree add`, plus a fresh untracked `local.properties` pointing at the local Android SDK.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: still optimistic (not re-fetched from the server) in both directions; a failed *unstar* attempt still collapses to the same flat `Failed` UI state as a failed star attempt, which can render as not-starred even if the repo is still actually starred server-side — accepted as a minor, harmlessly-retryable gap rather than adding more state to track which action was in flight.
- Next recommended task: get live confirmation that both starring (count up) and un-starring (count back down, tappable again) now work from the ALL list.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-034 third round: make the star/count update optimistic
- Agent and role: Claude, direct continuation of the same live session — with starring and un-starring both working correctly, the human product owner reported it still felt slow: "좋은데 바로바로 반응이 안되네 좀 늦게 카운팅이 변하는 느낌" (good, but doesn't respond immediately — the count feels like it changes a bit late).
- Root cause: every state/count update in `toggleFavorite` was applied only *inside* the coroutine, after `favoritesApi.star()/unstar()` had already returned — meaning the star/count visibly did nothing until the full network round trip to this app's home-NAS-hosted Gitea completed, the same underlying "this host is not a datacenter" latency class as TASK-031's HTTP timeout fix.
- How it was implemented: moved the `favoriteAction`/`starsCount` update to run synchronously in `toggleFavorite`, before `viewModelScope.launch { ... }` even starts, so the tap feels instant; the coroutine itself now only performs the actual `star()/unstar()` call. The catch block was extended to roll the optimistic change back (restore the pre-tap count, mark `Failed`) if the request actually turns out to have failed, so a failure doesn't leave the display stuck showing an action that never really happened. The FAVORITES-list unstar (which removes the whole row) deliberately stays non-optimistic, since reverting a *removed* row on failure is more jarring than reverting a color/count and wasn't part of what was reported as slow.
- Files changed: `RepositoryListViewModel.kt`, `RepositoryListViewModelTest.kt`, `tasks/034-favorite-star-count-not-incrementing.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing (new coverage: state/count updates immediately on tap, before the queued coroutine runs on the `StandardTestDispatcher`; a failed star and a failed unstar each roll the count back to its pre-tap value), 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: unchanged from earlier in this task — still optimistic rather than server-authoritative in both directions (accepted tradeoff), and the underlying "can't tell if already starred on load" limitation from TASK-018/TASK-030 is untouched.
- Next recommended task: get live confirmation that starring/un-starring now feels instant.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-035: remove the redundant per-file "커밋" button in the code browser
- Agent and role: Claude, direct continuation of the same live session — "현재 보고있는 페이지 우상단에 커밋 버튼은 이제 필요 없지 않나" (the Commit button in the top-right of the current page isn't needed anymore, is it).
- Commit/diff: on `claude/task-015-notifications`; `feature/codebrowser/CodeBrowserScreen.kt`, `feature/branchdetail/BranchDetailScreen.kt`, `strings_codebrowser.xml`, `tasks/035-remove-redundant-code-browser-commit-button.md`.
- Confirmed the observation was correct before acting on it: traced the header's "커밋" button (`onViewHistory`) all the way to its single call site in `BranchDetailScreen.kt` and found it just switched the pager to page 1 — the exact same action the top-level "커밋" tab's own `onClick` already performs a few lines above it. TASK-031's rename (both controls used to be independently labeled "히스토리"/differently-purposed before that) is what made the duplication visually obvious.
- How it was implemented: removed the button and the `onViewHistory` parameter threaded through `CodeBrowserScreen`/`BrowserHeader`, the call site in `BranchDetailScreen.kt`, and the now-unused `codebrowser_history` string resource (confirmed unused via a repo-wide search first). `BrowserHeader`'s title/ref layout simplified from a `Row`-wrapped `Column` to a plain `Column` now that there's no trailing button to share the row with. Also fixed a stale "History" word in a doc comment one line above (predating TASK-031's rename).
- Files changed: `CodeBrowserScreen.kt`, `BranchDetailScreen.kt`, `strings_codebrowser.xml`, `tasks/035-remove-redundant-code-browser-commit-button.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: None identified.
- Next recommended task: get live confirmation the header looks right without the button; otherwise continue per the standing direction on the rest of this session's queued items.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-036: push notification when a favorited repository gets a new push
- Agent and role: Claude, direct continuation of the same live session — building the last remaining big feature from earlier this session's queue, confirmed with the human product owner before starting given it needed a new permission and dependency.
- Commit/diff: on `claude/task-015-notifications`; new `core/notifications/` package (`FavoritePushDetector`, `LastSeenPushStore` + `SharedPreferencesLastSeenPushStore`, `FavoritePushNotifier`, `FavoritePushCheckWorker`, `FavoritePushScheduler`), two new use cases in `RepositoryBrowseUseCases.kt` (`GetLatestPushEpochMillisUseCase`, `ListFavoritedRepositoryCoordinatesUseCase`), `MainActivity.kt` (scheduling + runtime permission request), `AndroidManifest.xml` (`POST_NOTIFICATIONS`), a new notification icon and string resources, `gradle/libs.versions.toml`/`app/build.gradle.kts` (`androidx.work:work-runtime-ktx` 2.11.2), matching tests, `tasks/036-favorite-repo-push-notifications.md`.
- Feature implemented: a 15-minute `WorkManager` periodic job (network-constrained) checks every favorited repository's most recent push time — reusing the exact same "newest branch commit time" heuristic `RepositoryListViewModel` already uses for the repository list's own "마지막 push" row, now extracted as a shared, reusable `GetLatestPushEpochMillisUseCase` — and posts a local notification when it advances past what was last recorded.
- How it was implemented: `FavoritePushDetector` is pure comparison logic (no Android/network dependency), deliberately never reporting a repository as "newly pushed" on its first sighting (only a real advance past a previously-recorded timestamp counts), which prevents the very first background check this feature ever runs from firing one notification per already-favorited repository regardless of actual recent activity. Storage is `SharedPreferences` (one long per favorited repository), not Room, since `docs/architecture.md` reserves Room specifically for offline caching. The worker is scheduled unconditionally on every app launch (`ExistingPeriodicWorkPolicy.KEEP` makes repeat calls a no-op) and instead gates internally — it just returns success immediately when there's no stored session or no favorited repositories, so the scheduler itself doesn't need to track login state. `POST_NOTIFICATIONS` is requested at runtime on API 33+ from `MainActivity`; a denial degrades safely (the worker still runs and updates its baseline, `NotificationManagerCompat.notify()` is explicitly permission-checked before being called at all, matching its `@RequiresPermission` lint contract as a deliberate no-op rather than a caught exception).
- Files changed: see `tasks/036-favorite-repo-push-notifications.md`'s Scope/Writable paths sections for the full list.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing (new coverage: `FavoritePushDetectorTest` — first-sight never notifies, an actual advance does, unchanged/older doesn't, multiple repositories evaluated independently; new `RepositoryBrowseUseCasesTest` coverage for both new use cases, including multi-page pagination and a repository with no branches/no commit time), 0 lint errors. Installed on the human product owner's device over wireless `adb`; live-confirmed the `POST_NOTIFICATIONS` runtime prompt actually appeared and was granted (verified via `dumpsys package`), and app launch/scheduling produced no crash in `logcat`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: no deep link from the notification to the specific repository (opens the app at its normal entry point instead — a genuine follow-up-sized piece of work, not attempted here); no in-app way to disable this per-repository or change the interval; un-favoriting a repository drops its stored baseline on the next worker run, so re-favoriting it later starts fresh rather than retroactively notifying for what happened while un-favorited (a deliberate, simpler default). Actual end-to-end notification delivery (a real push landing, 15 minutes elapsing, a notification appearing) hasn't been observed yet in this session — that specifically doesn't fit inside one live-testing session's timeframe.
- Next recommended task: wait for a real push to a favorited repository and confirm the notification actually appears; otherwise this closes out the last big item from this session's queue — remaining work is live-confirmation of everything already built (TASK-030 through TASK-035, plus pinch-zoom/CSV/PDF/Issues-PR pagination from earlier).
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-036 follow-up: deep-link the favorite-push notification
- Agent and role: Claude, direct continuation of the same live session — "계속 이어서 개발해줘 멈추지말고" (keep developing, don't stop) after TASK-036's initial commit. Picked up the deep-link follow-up TASK-036 itself had explicitly flagged as scoped out ("no deep link yet... noted as a natural follow-up").
- Commit/diff: on `claude/task-015-notifications`; `RepositoryBrowseUseCases.kt` (`ListFavoritedRepositoryCoordinatesUseCase` → `ListFavoritedRepositoriesUseCase`, now returns full `RepositorySummary`), `FavoritePushNotifier.kt` (`notifyNewPush` takes an optional `defaultBranch`, attaches owner/name/ref string extras to the tap `PendingIntent`), `FavoritePushCheckWorker.kt` (looks up each repository's `defaultBranch` before notifying), `MainActivity.kt` (new `pendingFavoritePushRoute` Activity-property bridge + `consumeFavoritePushTarget`, wired into `onCreate`/`onNewIntent`), matching test updates, `tasks/036-favorite-repo-push-notifications.md` (updated throughout).
- Feature implemented: tapping the favorite-push notification now opens the app directly at that repository's file browser on its default branch, instead of just the app's normal entry point.
- How it was implemented: since the underlying push-detection only tracks one scalar "latest push time" per repository (not which branch produced it), the deep link targets the repository's *default* branch rather than the specific pushed branch — a correct, useful "take me to this repository" action even though it isn't always literally the branch that changed. `MainActivity` already had a `consumeCallback` pattern (read-and-clear one-shot intent data, originally for the OAuth redirect) that a new `consumeFavoritePushTarget` mirrors exactly. Since `routeStack` is Compose state scoped inside `setContent`, not a plain Activity field, the bridge from plain Activity code (`onCreate`/`onNewIntent`) into composition is a `mutableStateOf<Route.BranchDetail?>` held as a regular Activity property, read by a `LaunchedEffect` inside `setContent` that pushes it onto `routeStack` once set.
- Files changed: see `tasks/036-favorite-repo-push-notifications.md`'s Scope/Writable paths sections.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing (new coverage: `GetLatestPushEpochMillisUseCase`'s max-across-pages/no-commit-time/no-branches cases; `ListFavoritedRepositoriesUseCase`'s pagination and `defaultBranch` preservation), 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Live verification: simulated a notification tap directly via `adb shell am start` with the exact three extras `FavoritePushNotifier` would set — the app opened directly at `RobotAutomationDevelopment-Dept/LGES-OC5-PtoP-Welder`'s file browser on branch `main`, screenshot-confirmed, no crash. This proves the deep-link *navigation* half works correctly; the actual trigger half (a real push firing the notification organically) still isn't observed within this session's timeframe.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: deep link targets the default branch, not necessarily the specifically-pushed branch (by design, see above); a repository with no default branch still notifies, just without deep-link extras.
- Next recommended task: this closes out the last explicitly-scoped item from this session's feature queue. Remaining work is live-confirmation of everything already built across this whole session (TASK-027 through TASK-036) — pinch-zoom/CSV/PDF viewers, Issues/PR pagination, the green theme, and this notification feature's actual end-to-end firing.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-037: already-starred repositories show filled on the ALL list's first load
- Agent and role: Claude, direct continuation of the same live session — "계속 이어서 개발해줘 멈추지말고" (keep developing, don't stop) after TASK-036's deep-link follow-up. Picked up a real, previously-accepted-not-fixed known limitation from TASK-018/TASK-030 rather than waiting on further live confirmation.
- Commit/diff: on `claude/task-015-notifications`; `feature/repositories/RepositoryListViewModel.kt` (+ test additions), `tasks/037-already-starred-repos-show-filled-on-load.md`.
- Feature implemented: the ALL repository list now shows a filled star from its very first load for any repository the account has already starred — not just after this session's own toggle interaction.
- How it was implemented: `loadStarredCoordinatesOnce()` fetches the account's full starred-repository set once per session via the same `/user/starred` endpoint already used for the FAVORITES tab (Gitea's plain repository-list response has no "is this starred by me" field, and calling a per-repository IsStarring endpoint once per row would have turned one list load into N+1 requests) — mirrors `loadOrganizationsOnce`'s exact shape (cache-check, non-fatal try/catch fallback). `toggleFavorite`'s success paths now also keep this cache directly in sync (`+`/`-` the toggled repository) so later reloads within the same session stay consistent without a re-fetch; `refreshRepositories()`/`logout()` invalidate/clear it the same way the equivalent `repositorySnapshots`/`cachedOrganizations` caches already are.
- Files changed: `RepositoryListViewModel.kt`, `RepositoryListViewModelTest.kt`, `tasks/037-already-starred-repos-show-filled-on-load.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all 28 tests in `RepositoryListViewModelTest` passing including every pre-existing one unmodified (new coverage: an already-starred repository shows filled on first load while a sibling shows outlined; un-starring then reloading the ALL list confirms the cache itself, not just the visible row, updated), 0 lint errors. Installed on the human product owner's device.
- Live verification: confirmed `RobotAutomationDevelopment-Dept/LGE-OC6-Inline-CylinderTypeWelder` was genuinely starred server-side via the FAVORITES tab (★ 1 shown there), then located it on the ALL list (page 2) after a fresh app launch — showed filled green ★ 1 immediately with no tap needed, screenshot-verified, no crash.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: still session-scoped (re-fetches on a fresh session, same as every other cached list in this app); doesn't reflect a starred-set change made elsewhere (Gitea web, another device) until the next pull-to-refresh or re-login.
- Next recommended task: this closes a real, previously-documented gap rather than adding new surface area. Remaining work across this whole session is now mostly live-confirmation of already-built features (TASK-027 through TASK-036) that haven't been directly exercised by the human product owner yet.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-028 follow-up: visible feedback for an out-of-range page-jump
- Agent and role: Claude, direct continuation of the same live session — "계속 이어서 개발해줘 멈추지말고" (keep developing, don't stop). Picked up a small, contained item TASK-028 itself had documented as a known limitation.
- Commit/diff: on `claude/task-015-notifications`; `feature/workitems/WorkItemsScreen.kt`, `strings_workitems.xml`, `tasks/028-issues-prs-numbered-pagination.md`.
- Fix implemented: the Issues/PR numbered-pagination page-jump field previously accepted any typed number and silently clamped it to `[1, totalPages]` only on submit (typing "999" would quietly land on the last page with no explanation). It now sets `isError`/`supportingText` on the field whenever the currently-typed value falls outside that range, making the clamp visible before the tap instead of after.
- How it was implemented: pure Compose-only change (`isOutOfRange` derived from the field's own current text, compared against `totalPages`) — submitting still clamps exactly as before, this only adds visible feedback. No test coverage added since this file's pagination UI has never had dedicated tests (matches this codebase's established pattern of not unit-testing pure Compose screens).
- Files changed: `WorkItemsScreen.kt`, `strings_workitems.xml`, `tasks/028-issues-prs-numbered-pagination.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, all unit tests passing, 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: same as the rest of TASK-028 — not yet seen live against a repository with enough real issues/PRs to populate the numbered UI, since no such repository was found this session.
- Next recommended task: this was a small, contained polish item. Larger remaining candidates (e.g. a reactive 401-retry fallback at the network layer, per-repository notification settings) were considered but judged too large/risky for unrequested autonomous continuation without a dedicated task scoping pass — flagging them for the human product owner's direction rather than proceeding further unrequested into security-adjacent or larger-surface-area territory.
- Next owner: Claude, continuing per the standing direction once the human product owner responds, or awaiting live confirmation of everything already built this session.
## 2026-08-29 (continued) — TASK-038: Settings screen with opt-in auth-retry and a favorite-push toggle
- Agent and role: Claude, direct continuation of the same live session. Presented two candidate next-steps (reactive 401 retry / per-toggle push notification settings) and their tradeoffs; the human product owner asked for both, with the reactive retry explicitly framed as something the Settings screen should gate ("설정화면에서 설정 해두면 동작하고 안해두면 동작 안하게").
- Commit/diff: on `claude/task-015-notifications`; new `core/settings/` (`AppSettingsStore`, `SharedPreferencesAppSettingsStore`, `AppSettingsSession`), new `feature/settings/` (`SettingsViewModel`, `SettingsScreen`), `Route.Settings` + `RouteStackSaver`, a "설정" button on `RepositoryListScreen`'s top bar, new `ReactiveTokenProvider` interface + shared `callAuthenticated` helper in `GiteaRepositoryBrowserApi.kt`, `RefreshingTokenProvider.forceRefresh()`, all four Gitea API classes (`GiteaRepositoryBrowserApi`/`GiteaHistoryApi`/`GiteaWorkItemsApi`/`GiteaNotificationsApi`) gained a defaulted `reactiveAuthRetryEnabled` parameter, all five `ViewModel.Factory`s that construct one of them wired to the new setting, `FavoritePushCheckWorker` gated on the push-notification toggle, matching tests, `tasks/038-settings-screen-reactive-auth-retry-push-toggle.md`.
- Feature implemented: (A) an opt-in (default off) reactive fallback — a live 401 despite proactive refresh having judged the token fine now optionally retries once with a forcibly-refreshed token before falling back to the existing re-login flow; (B) TASK-036's background push checker can now be turned off entirely from a new Settings screen, closing that task's own documented limitation.
- How it was implemented: `ReactiveTokenProvider` is a separate interface from the existing `fun interface BearerTokenProvider` (not an addition to it), specifically so every existing test fake written as a plain `BearerTokenProvider { "token" }` lambda keeps compiling — only `RefreshingTokenProvider` also implements it, and a new shared `callAuthenticated` helper checks `tokenProvider as? ReactiveTokenProvider` before relying on it, falling back to the old single-attempt behavior otherwise. All four API classes' near-identical `authenticated`/`get` wrappers now delegate to this one shared helper instead of duplicating the retry orchestration four times. `forceRefresh()` always attempts a refresh regardless of stated expiry (unlike the existing proactive `accessToken()`, which would just hand back the same still-time-valid-looking token again), reusing the exact same mutex and refresh-or-fall-back logic (factored into a shared private `refreshOrFallBack`) so a reactive retry racing an in-flight proactive refresh still shares one attempt at the single-use rotating refresh token. Every new constructor parameter defaults to preserving old behavior exactly, so no existing call site or test needed to change.
- Files changed: see `tasks/038-settings-screen-reactive-auth-retry-push-toggle.md`'s Scope/Evidence sections for the full list.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug` BUILD SUCCESSFUL, every pre-existing test across all four API classes and `RefreshingTokenProvider` passing unmodified, plus new coverage (`forceRefresh()`'s behavior, `callAuthenticated`'s retry decision matrix, a `Route.Settings` round-trip), 0 lint errors. Installed on the human product owner's device over wireless `adb`.
- Live verification: opened Settings from the new button; both switches visibly toggled (screenshot-confirmed each state change, including exact tap-coordinate discovery via `uiautomator dump` after an initial miss); backed out and re-entered Settings, confirming both toggles persisted via `SharedPreferences`; restored both to their defaults afterward; `logcat` clean of any crash throughout.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: reactive auth retry's actual live effect (recovering from a genuine 401 without forcing re-login) hasn't been observed end-to-end, since that requires a rare, hard-to-trigger real revocation event — covered instead by direct unit tests of the retry orchestration. Favorite push notifications remain one app-wide toggle, not per-repository (deliberate scope decision, not an oversight).
- Next recommended task: this closes the two explicitly-requested items from this "continue developing" stretch. Remaining work across the whole session is largely live-confirmation of already-built features that haven't been directly exercised yet (pinch-zoom/CSV/PDF viewers, Issues/PR numbered pagination with real data, the actual favorite-push notification firing end-to-end).
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-039: Design pass — app icon, visibility badge, header truncation, PR-label Korean-ization
- Agent and role: Claude, direct continuation of the same live session. The human product owner asked
for an open-ended design review ("디자인 수정해줄래? 앱의 아이콘부터 해서 전체적으로 확인해보고
부족한 부분 고쳐줘") after all previously-requested features were built. Proceeded autonomously,
screen by screen, per the standing direction.
- Commit/diff: on `claude/task-015-notifications`; new launcher-icon drawables and `mipmap-anydpi-v26`
wrappers, `AndroidManifest.xml`'s `android:icon`/`android:roundIcon`, `RepositoryListScreen.kt`'s
new `VisibilityBadge` composable and references-header title truncation, `strings_workitems.xml`/
`strings_notifications.xml`'s `Pull Request` → `PR`, `tasks/039-design-pass-app-icon-and-visual-polish.md`.
- Feature implemented: (1) a custom adaptive app icon replacing Android's bare default (an original
green git-branch glyph matching the app's own primary color, not a reproduction of Gitea's logo);
(2) a compact styled badge for the repository-list visibility label, replacing an unstyled
full-width text line; (3) 2-line ellipsis truncation on the references screen's repository-name
header, which a real long-named repository was wrapping to 3 lines; (4) "Pull Request" → "PR" in
the two string resources that were the only English text left in an otherwise fully-Korean UI —
also the cause of a visible line-wrap on the live notifications list, since it was the widest word
in a `Row` with no wrap handling.
- How it was implemented: the icon uses only vector primitives (arc-path circles, stroked lines), no
icon library, matching this app's established convention. `VisibilityBadge` reuses the same
`Surface` + `RoundedCornerShape` + `labelSmall` construction already used for `FilterChip`
elsewhere on the same screen. "PR" was already this app's own convention in five other strings
(`workitems_pr_refs`, `workitems_permission_denied`, the "PR/이슈" button label, etc.) — only the
tab label and the notifications subject label had been left as the English full phrase.
- Files changed: see `tasks/039-design-pass-app-icon-and-visual-polish.md`'s Scope/Evidence sections.
- Checks and results: `./gradlew compileDebugKotlin testDebugUnitTest assembleDebug lintDebug` BUILD
SUCCESSFUL (run twice, once per round of changes), all unit tests passing, 0 lint errors.
- Live verification: installed on the human product owner's device over wireless `adb` after each
round. Icon confirmed via the Recents/task-switcher view. Badge and header truncation confirmed via
screenshot on a real long-named repository. PR-label fix confirmed via screenshot against the
notifications "전체" tab with real PR/issue data — every row now fits "PR" on one line alongside
its state label instead of wrapping. Dark mode forced via `adb shell cmd uimode night yes` and
reviewed for the repository list and references screens (no contrast issues found), then restored.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: work-items/code-browser screens still don't show their own repository/branch
context in a header (needs ViewModel-layer changes, scoped out of this visual-polish batch);
dark-mode review only covered two of the app's screens so far; the code browser, commit
history/diff, and work-item/PR detail views with real content haven't been reviewed yet — this is
the first batch from an ongoing design-review pass, not a complete one.
- Next recommended task: continue the design review into the code browser (file tree, text/image/CSV/PDF
viewers), commit history/diff view, work-item/PR detail screens with real content, and extend the
dark-mode check to the remaining screens; separately, consider a small follow-up task to surface
repository/branch context in the work-items and code-browser headers.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-039 follow-up: extended design review, no further changes needed
- Agent and role: Claude, direct continuation of the same live session, continuing the design-review
pass per the standing direction after committing TASK-039's first batch of fixes.
- What was reviewed: the code browser's file tree (폴더/파일 rows, breadcrumb), the commit history
list, and the per-file diff view (tapping into an individual file's change from a commit) — both in
light and dark mode (`adb shell cmd uimode night yes`/`no`).
- Outcome: no code changes needed. The per-file diff view already does proper git-style red/green
color-coding (`DiffAddedColor` for additions, `MaterialTheme.colorScheme.error` for removals) in a
side-by-side 기존(before)/변경(after) layout with a monospace font, and it holds up cleanly in dark
mode too. The file tree and commit list are visually consistent with the rest of the app's list-row
pattern (title + metadata, no icon library, matching the app-wide convention).
- Files changed: `tasks/039-design-pass-app-icon-and-visual-polish.md`'s Evidence/Known limitations
sections updated to record this review pass; no source changes.
- Next recommended task: continue into work-item/PR detail views with real content and the text/image/CSV/PDF
file viewers, and extend the dark-mode pass to notifications/settings/work-items.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-040: repository-list header overflow menu (code-only, no device install)
- Agent and role: Claude, direct continuation of the same live session. The human product owner asked
to stop growing the repository list's top bar with more always-visible buttons and instead collapse
설정/알림/로그아웃 behind a hamburger-style overflow menu, and explicitly asked for code only this
round with wireless debugging paused ("잠시 무선 디버깅 하지말고 코드만 수정해줘").
- Commit/diff: on `claude/task-015-notifications`; `RepositoryListScreen.kt`'s header now shows one
"☰" `IconButton` opening a `DropdownMenu` (설정/알림/로그아웃) instead of three separate
`OutlinedButton`s; `tasks/040-repository-list-header-overflow-menu.md`.
- How it was implemented: new private `OverflowMenuButton` composable takes the exact same three
callbacks (`onViewSettings`/`onViewNotifications`/`onLogout`) the header already received —
presentation-only change, no navigation/ViewModel/use-case code touched. Uses the plain "☰" Unicode
glyph rather than a drawable icon, consistent with this app's established no-icon-library
convention (the same convention already used for ★/☆ elsewhere).
- Files changed: `feature/repositories/RepositoryListScreen.kt`, `tasks/040-repository-list-header-overflow-menu.md`.
- Checks and results: `./gradlew compileDebugKotlin testDebugUnitTest assembleDebug lintDebug` BUILD
SUCCESSFUL, all unit tests passing unmodified, 0 lint errors.
- Live verification: intentionally not done this round — wireless debugging was paused at the human
product owner's request. Code is ready to install and screenshot-verify (light and dark mode) next
session.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: not yet live-confirmed on-device (see task file).
- Next recommended task: once wireless debugging resumes, install and screenshot-verify the new
overflow menu (button sizing/position, dropdown legibility, correct navigation for each item, both
light and dark mode), then continue the broader design-review pass (work-item/PR detail views, file
viewers) from where TASK-039 left off.
- Next owner: Claude, continuing per the standing direction once wireless debugging resumes or further
code-only work is requested.
## 2026-08-29 (continued) — TASK-040 follow-up: live-verified once wireless debugging resumed
- Agent and role: Claude, direct continuation of the same live session. The human product owner said
"다시 해줘" once wireless debugging was back on, picking up exactly where TASK-040 left off (install
and live-verify the code-only overflow-menu change).
- Commit/diff: on `claude/task-015-notifications`; `tasks/040-repository-list-header-overflow-menu.md`
updated from "not yet live-verified" to confirmed, with the live-verification evidence recorded.
- Live verification: installed the already-built APK over wireless `adb`. Screenshot-confirmed the
"☰" button renders correctly next to the title in both portrait and the foldable's unfolded
landscape layout; opened the dropdown and confirmed all three items (설정/알림/로그아웃) render and
are positioned correctly; tapped 설정 and confirmed it navigates to the Settings screen exactly like
the old button did. Repeated the same check with `adb shell cmd uimode night yes` — both the button
and dropdown hold good contrast in dark mode — then restored `night no`.
- Files changed: `tasks/040-repository-list-header-overflow-menu.md` only; no source changes (the
code from the previous entry was already correct).
- Next recommended task: continue the broader design-review pass (work-item/PR detail views with real
content, file viewers) from where TASK-039 left off.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-041: favorite star reverted / missing from favorites after tab switch
- Agent and role: Claude, direct continuation of the same live session. Live bug report mid-turn while
reviewing another screen: "즐겨찾기 누르고나면 카운팅은 되는데 즐겨찾기 탭에 가면 방금 누른 항목이
안떠있다. 그리고 다시 원래 탭인 레포지토리로 가보면 다시 즐겨찾기가 풀려있어." Paused the in-progress
work and switched to reproducing and fixing this immediately, per the standing priority of live bug
reports over open-ended review work.
- Commit/diff: on `claude/task-015-notifications`; `RepositoryListViewModel.kt`'s `toggleFavorite`
reworked to patch `repositorySnapshots` (not just `starredCoordinatesCache`) on every successful
star/unstar; two new tests in `RepositoryListViewModelTest.kt`;
`tasks/041-favorite-star-reverts-after-tab-switch.md`.
- Root cause: `repositorySnapshots` (the per-mode full-repository-list cache, fetched once per session
and reused on every later visit to that mode) was never patched by `toggleFavorite` — only the
separate `starredCoordinatesCache` was. So the star icon correctly showed filled (from the patched
cache), but the displayed `starsCount` reverted to whatever was cached from the very first load of
that mode, and a favorites tab already cached from an earlier visit kept silently missing a
newly-starred repository.
- How it was diagnosed: reproduced live on-device (star a repository, switch to 즐겨찾기, switch back —
watched the star/count go wrong exactly as reported). Added temporary debug-only `Log.w` logging of
the star/unstar HTTP response directly in `GiteaRepositoryBrowserApi` to rule out a failed request —
confirmed `HTTP 204` (success) every time, isolating the bug to client-side caching, not the network
call. Removed the temporary logging once the root cause was confirmed.
- How it was fixed: tried a "clear the whole cache on every toggle, force a refetch" approach first;
reverted after live testing showed Gitea's own starsCount on the plain repository-listing endpoint
lags slightly behind a star/unstar (the /user/starred listing reflected a toggle sooner) — forcing a
refetch would trade one race condition for another. Switched to a local patch instead: two new
private helpers (`adjustCachedStarCounts`, `removeFromCachedSnapshot`) keep every cached snapshot
that includes the toggled repository consistent with what the app already knows happened, without a
network round trip. A star also invalidates just the FAVORITES snapshot key if it was already cached
(can't surgically insert a row without a full domain object at that call site) so the next visit
fetches it fresh instead of silently missing the new star.
- Files changed: `feature/repositories/RepositoryListViewModel.kt`,
`feature/repositories/RepositoryListViewModelTest.kt` (test file, actually under
`app/src/test/java/...`), `tasks/041-favorite-star-reverts-after-tab-switch.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug`
BUILD SUCCESSFUL, every pre-existing favorite-toggle test passing unmodified plus two new regression
tests, 0 lint errors.
- Live verification: installed over wireless `adb`. Starred a never-before-touched repository
(`LGD-AP4-RPR`) from the ALL tab, confirmed it appeared in 즐겨찾기 (★1), switched back to 전체
저장소 and confirmed the star/count stayed correct (★1) instead of reverting — screenshots at each
step.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: None outstanding for this task.
- Next recommended task: resume the design-review pass (TASK-039's known limitations: work-item/PR
detail views with real content, file viewers, extended dark-mode coverage) from where it was paused
for this bug fix.
- Next owner: Claude, continuing per the standing direction.
## 2026-08-29 (continued) — TASK-042: colored status badges for issue/PR state
- Agent and role: Claude, direct continuation of the same live session, per "쭉 이어서 개발이랑 디자인
진행해줄래?" — resuming the paused design-review pass named as next work in TASK-041's log entry.
- Investigation: first re-verified the review item flagged as unresolved at the pause point — whether
issue/PR body text renders correctly for real content (the one PR viewed earlier happened to have no
body). Opened the real "PostActivated 관련 추가 필요" issue (multi-paragraph Korean body plus an
embedded C# code block) and confirmed it renders correctly end to end — no bug found there. While
doing that, noticed the 열림/닫힘/병합됨 state label next to it (and next to every issue/PR, in both
list rows and detail headers) was a plain, uncolored `bodySmall` line, no more visually prominent
than the `#8` issue number beside it — inconsistent with the colored `VisibilityBadge` treatment
TASK-039 already gave 공개/비공개 on the repository list.
- Commit/diff: on `claude/task-015-notifications`; `feature/workitems/WorkItemsScreen.kt` gained a
`StatusBadge` composable (two thin state-typed overloads sharing one rendering implementation)
replacing the four plain-text state-label call sites (`IssueRow`, `PullRequestRow`,
`IssueDetailView`, `PullRequestDetailView`); `tasks/042-workitem-status-badges.md`.
- How it was implemented: reused existing Material3 theme roles rather than inventing new colors —
OPEN gets `primaryContainer` (the same green `VisibilityBadge` uses for a public repository), CLOSED
gets the baseline `errorContainer` `Theme.kt` already reserves for closed/removed states, and MERGED
(a third state GitHub/Gitea conventionally color purple, which this theme has no role for) gets the
teal `tertiaryContainer` instead. Both light and dark mode are handled automatically since no new
hardcoded color was introduced.
- Files changed: `feature/workitems/WorkItemsScreen.kt`, `tasks/042-workitem-status-badges.md`.
- Checks and results: `./gradlew compileDebugKotlin compileDebugUnitTestKotlin testDebugUnitTest assembleDebug lintDebug`
BUILD SUCCESSFUL, all existing unit tests passing unmodified (a pure presentation change with no new
branching logic beyond the enums' own exhaustive `when`s), 0 lint errors.
- Live verification: installed over wireless `adb`. RobotAutomationDevelopment-Dept/OC5-PilotLine-
RectangleTypeWelder PR tab: confirmed a teal "병합됨" badge on two merged PRs and a green "열림" badge
on the third, side by side (screenshot); opened the merged PR's detail view and confirmed the same
teal badge in the header (screenshot). Repeated with `adb shell cmd uimode night yes` — both badge
colors keep good contrast against the dark surface (screenshot) — then restored `night no`.
- Review findings and resolutions: None from a separate review pass.
- Known limitations: `NotificationsScreen.kt` renders its own separate PR/이슈 상태 column as plain
text — the same visual gap, in a different screen, not touched in this pass to keep it scoped to
what was actually being reviewed. Did not visually confirm the red CLOSED badge with this fix
installed (no real closed issue/PR was opened live this session) — low risk, since the color mapping
reuses the same `errorContainer` role already used unconditionally elsewhere in the app.
- Next recommended task: continue the design-review pass — the text/image/CSV/PDF file viewers (not
yet reviewed this session), and consider extending today's status-badge fix to
`NotificationsScreen.kt` for consistency.
- Next owner: Claude, continuing per the standing direction.