Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .claude/skills/retro/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ This affects Steps 3 and 5 below. All other steps run the same regardless of mod
- Find the transcript: glob `~/.claude/projects/<converted-cwd>/*.jsonl` sorted by modification time (convert cwd slashes to dashes, e.g., `/Users/me/myproject` → `-Users-me-myproject`). Pick the most recent.
- Run the script: `python3 scripts/analyze_transcript.py <path-to-jsonl>` — the script lives in `scripts/` alongside this SKILL.md.
- The script outputs: per-turn breakdown (full user text, assistant word count, tools, errors) and timing stats (reading at 150 wpm, typing at 60 wpm, 1 min buffer per turn, overlapping turns merged).
- System-injected messages (skill injections, /mcp outputs, system reminders) are automatically filtered out.
- System-injected messages (skill injections, /mcp outputs, system reminders) are automatically filtered
out, as are the agent's own image reads -- a screenshot the agent took arrives as a `user` message and
would otherwise be counted as a prompt the human typed, inflating hands-on time.

**What you do with the output:**
- Read the turn-by-turn output to understand what happened
Expand All @@ -39,7 +41,7 @@ This affects Steps 3 and 5 below. All other steps run the same regardless of mod

Present as a time breakdown table with proportional bars and a metrics summary:

| Started | Phase | 👤 Hands-On Time | 🤖 Agent Time | Problems |
| Started | Phase | 👤 Hands-On Time | 🤖 Span (agent + away) | Problems |
|---------|-------|-----------------|---------------|----------|
| Feb 10 10:00am | Build (engine restart, voice recog, UI tweaks) | ██████ 60m | ███ 30m | ⚠ 5 fix cycles |
| Feb 10 11:30am | Research (BT routing for AirPods + external mics) | | █████ 45m | |
Expand Down Expand Up @@ -127,7 +129,7 @@ This affects Steps 3 and 5 below. All other steps run the same regardless of mod
## YYYY-MM-DD - [Brief context of what we worked on]

### Time Breakdown
| Started | Phase | 👤 Hands-On Time | 🤖 Agent Time | Problems |
| Started | Phase | 👤 Hands-On Time | 🤖 Span (agent + away) | Problems |
|---------|-------|-----------------|---------------|----------|
| ... | ... | ... | ... | ... |

Expand Down
6 changes: 6 additions & 0 deletions .claude/skills/retro/scripts/analyze_transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
- Skill injections ("Base directory for this skill:")
- Local command outputs (<command-name>, <local-command-)
- System reminders (<system-reminder>)
- Image tool results ("[Image: original ...]"), which are the agent's own Read of a
screenshot arriving in the human role -- counting those as human turns adds reading
and buffer time nobody spent. Measured on one session that drove a device: 7.5 min of raw
hands-on, 4.5 min adjusted, all of it typing and per-turn buffer. Reading time does not
change -- the assistant words are conserved, just re-attributed to the prompt that caused them.
"""

from __future__ import annotations
Expand Down Expand Up @@ -55,6 +60,7 @@

SYSTEM_MESSAGE_PATTERNS = [
re.compile(r"^Base directory for this skill:"),
re.compile(r"^\[Image: original \d+x\d+"),
re.compile(r"^<(command-name|local-command|system-reminder)"),
re.compile(r"^<local-command-caveat>"),
re.compile(r"^This session is being continued from a previous conversation"),
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ See **[ARCHITECTURE.md](ARCHITECTURE.md)** — the single source of truth for th
- **Protect the two Android system bars** in any UI work: the top status bar (clock, notifications, status icons) and the bottom navigation bar (home, back, recents). Don't draw over or intercept them.
- **Every screen must survive 2x font scale.** Users with low vision run large system fonts, and a screen that clips or hides content at 2.0 is broken for them. Verify any new or changed screen at font scale **1.0 and 2.0** (see Build & test, Emulator / device) and say in the PR that you did. Text grows, so: use `sp` for text and `dp` for spacing — never an `sp` dimen as a margin or padding; don't box text in a fixed `dp` height or width; give content that can grow somewhere to scroll; and reserve `maxLines`/`singleLine`/`ellipsize` for text that is genuinely disposable.
- **Plan and size before building.** Prefer **one PR per ticket/use case** — don't force-split a coherent change (splitting has its own overhead when later edits span the pieces). When a change is large, break it into **reviewable commits** — mechanical/refactor commits separate from behavioral ones — and offer review-by-commit. Treat ~500 LOC / ~10 files as a signal to reach for that commit structure, not a hard cap; the ceiling rises as LLM-assisted review matures. For staged multi-commit refactors (e.g. removing a dependency across many files/modules), order stages easiest-to-hardest and independently compile/test each stage (see Build & test's fast-iteration guidance) before moving to the next, so a failure is isolated to the stage that caused it.
- **Before designing a way to make a costly operation cheaper, ask whether the operation is needed at all.** When a problem's severity scales with how often something happens — connections opened per second, requests issued per page, calls made per frame — eliminating the operations usually beats optimizing each one. Check whether the platform offers a way to do the work without them before writing the plan for the optimized version. ADFA-5172/5176 is the cautionary case: the server opened one TCP connection per documentation request to our own process, and `WebViewClient.shouldInterceptRequest` served the same content with no connection at all, which HTTP keep-alive would only have made cheaper per connection.
- **Keep docs in step with code.** When you change code, update the docs that describe it in the same change — a module's `README.md`, `ARCHITECTURE.md`, or an ADR — so a doc never outlives the API it documents (see REVIEW.md, Code quality). If the doc fix is out of scope, file a ticket rather than let it drift.
- `.androidide_root` is a sentinel file tests use to locate the project root — don't delete it.
- Avoid http or https links which go off-device. When such links are unavoidable, warn the user beforehand and offer to cancel the action.
Expand All @@ -65,6 +66,8 @@ See **[ARCHITECTURE.md](ARCHITECTURE.md)** — the single source of truth for th

**Tabs** for indentation, **LF** line endings — enforced by **Spotless**. The `ratchetFrom = origin/stage` ratchet is **file-level, not line-level**: it checks every file that differs from `origin/stage` and reformats each such file *in full*, so editing even one line of a file whose existing indentation doesn't conform (e.g. a layout XML using 4 spaces) pulls the **whole file** under the ratchet and requires reindenting it to tabs — a one-line edit can become a whole-file reformat. Java uses the **Eclipse** formatter (`spotless.eclipse-java.xml`, with member sorting + import ordering); Kotlin and `*.gradle.kts` use **ktlint**; XML uses the **Eclipse WTP** formatter. Run `./gradlew spotlessApply` to fix formatting before pushing — the `.githooks` pre-push hook does this automatically once hooks are installed and enabled (`sh ./scripts/install-git-hooks.sh`, no conflicting `core.hooksPath`). Branch names must match `.../ADFA-#####` (3–5 digits) — see CONTRIBUTING.md; a pre-commit hook enforces it (`sh ./scripts/install-git-hooks.sh`).

When the ratchet pulls a whole file in, **land that reformat as its own commit, before the behavioral one**. A 160-line whitespace diff sitting on top of a 10-line change hides the change; split, and the reviewer reads what matters. Say so in the reformat commit's message so nobody hunts for behavior in it.

Keep docs, tickets, commit messages, and PR descriptions crisp — say it once, lead with the point, cut hedging and restated context. Brevity is the soul of wit; a reader's attention is the scarce resource.

**Code comments** follow the same discipline:
Expand Down
11 changes: 11 additions & 0 deletions docs/process/learnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,23 @@
- Before pushing a follow-up commit to a community PR, check `gh pr view <n> --json headRepositoryOwner` — the PR head is usually on the contributor's **fork**, so a same-named push to `origin` doesn't touch the PR and just creates a confusing dead branch that has to be deleted.

## Android / Kotlin
- A config data class whose **default** values call framework APIs (e.g. `ServerConfig`'s paths default to `Environment.getExternalStorageDirectory()`) makes itself unconstructable in a JVM unit test that doesn't stub those APIs — `RuntimeException: Method ... not mocked`, thrown from the constructor before your test body runs. Such a test has to pass *every* framework-backed parameter explicitly (or `mockkStatic` the API, or run under Robolectric), which is easy to miss when copying a config from a test that already does. Prefer lazily-resolved paths in new config types.
- `Handler.removeCallbacks(Runnable)` only removes callbacks posted by that *exact* `Handler` instance, not just the same `Looper` — `Handler(Looper.getMainLooper()).removeCallbacks(x)` won't cancel something posted via a *different* `Handler` bound to the same looper. Any post/cancel pair needs to share one `Handler` instance (see `TaskExecutor.mainThreadHandler`, added when replacing blankj's `ThreadUtils.getMainHandler()`).

## Serving content to a WebView
- A WebView can be handed content **in-process** through `WebViewClient.shouldInterceptRequest`, returning a `WebResourceResponse` built from a stream — no socket, no port, no handshake. It sees the http(s) requests the page makes, whatever the URL, so an existing `http://localhost:PORT/...` URL space needs **no rewriting**: strings.xml entries, link builders and even a published plugin-API contract keep working while the transport underneath changes (ADFA-5176 turned 31 TCP connections per documentation page into 0 this way). It is not a hook on *every* URL — `blob:`, `javascript:` and `file:///android_asset/` requests don't reach it — so anything routed that way still needs its own path.
- A WebView does **not** decode an intercepted response, so hand back decompressed bytes and don't bother with `Content-Encoding`. Give `WebResourceResponse` the bare MIME type with the charset as its own argument, and pass `null` for binary types — claiming a charset on an image makes the WebView try to decode it as text.
- `shouldInterceptRequest` never sees a POST body: a request with one has to go to the network. Range requests are answerable in principle — `WebResourceResponse`'s six-argument constructor takes a status code and headers, so 206 is expressible — but a handler that ignores `Range` and returns 200 with the whole body is not; ours does, which was fine because the WebView asked for a whole 407 KB PDF rather than ranges.
- A WebView does not render PDFs itself: pointing one at an `application/pdf` URL shows a blank page, identically over HTTP or in-process (verified on Android 13 with byte-identical screenshots). Showing one in-app needs a real renderer — `PdfRenderer`, or handing the file to an external viewer. See ADFA-5180.

## Android system SQLite
- Don't assume the JSON1 extension. On a Samsung Android 13 device, `JSON_OBJECT`/`JSON_GROUP_ARRAY` fail at runtime with `no such function: JSON_OBJECT` even though the same query runs fine against the same database file under a desktop sqlite3. Any query using JSON functions needs either a fallback or a documented minimum, and a JSON-based endpoint can be dead on real hardware while passing every desktop test.

## Reverse-engineering a library before porting it
- When writing a same-name drop-in for a third-party utility (to remove the dependency without changing call-site behavior), don't guess its semantics from memory/docs — extract the AAR's `classes.jar` and run `javap -c` against the actual bytecode to confirm exact chaining/wrapping behavior, especially for fluent/reflection-style APIs where a subtle mismatch (e.g., wrapping a field's *declared* type vs. its *runtime* class) changes behavior at existing call sites.

## MockK
- To unit-test code that touches WebView plumbing without Robolectric: `mockkStatic(android.os.Environment::class)` for `getExternalStorageDirectory()`, and a plain `mockk<Uri>` stubbing only `host`/`port`/`path`. Keep the framework *construction* out of the unit under test — a `WebResourceResponse` constructor throws `Stub!` in a JVM test, so split the decision (which content answers this request) from the wrapping, and test the decision.
- Migrating a mocked call from a Java static method (`mockkStatic(SomeClass::class)`) to a Kotlin top-level extension function requires `mockkStatic("com.package.FileNameKt")` (the compiled JVM facade class name) instead — `mockkStatic(ExtensionReceiver::class)` doesn't work for extension functions.

## Measuring a real before/after delta
Expand Down
Loading
Loading