Skip to content

fix(mobile): derive syntax palette from color scheme, not background hex - #6188

Merged
iscekic merged 1 commit into
mainfrom
kwf/janitor-2026-09-15-experience-and-design-c99d
Sep 17, 2026
Merged

iscekic merged 1 commit into
mainfrom
kwf/janitor-2026-09-15-experience-and-design-c99d

Conversation

@iscekic

@iscekic iscekic commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

Not verified. kwf reopened this section on 2026-09-16: verification restarted (phase verify)
The proof below came from an earlier round and can be stale. Do not review until this note is gone.

Changelog for users

  • Code blocks in session transcripts and PR review diffs keep the correct syntax colors in light and dark mode on iOS and Android.
  • Those colors now follow the device color scheme, so a future palette change cannot silently paint light syntax colors on a dark surface.

Changelog for maintainers

  • Dark detection in code-block.tsx, diff-line.tsx, and pr-diff-side-by-side-row.tsx now uses useColorScheme() === 'dark' instead of the colors.background === '#0E0E10' equality.
  • Rendered colors are unchanged for the current light and dark palettes; no token or palette value changed.
  • syntax-colors.ts values, src/global.css, and theme-colors.generated.ts are untouched, and no dependency was added.
  • Review the three isDark sites first; they are the only production change.
  • New suites in code-block.test.ts, diff-line.mounted.test.tsx, and pr-diff-side-by-side-row.mounted.test.tsx assert the dark palette under useColorScheme() === 'dark' and the light palette under 'light', comparing against tokenColorFor and MUTED_COLOR rather than any hex literal.
  • Existing suites that stub react-native (markdown-renderer.test.ts, tool-diff-preview.test.ts) now provide useColorScheme because the components read it.
  • Risk: these renderers now require useColorScheme in any test or host that mocks react-native; a missing stub returns undefined and selects the light palette.
  • The side-by-side test helper passes a language so token runs are actually painted, and the code-block suite re-checks the palette on a later render.

E2E proof

[e1] PR 6188 own scenarios on android: dark PR-review unified diff (e3), appearance flip with a diff on screen (e6), transcript code block (e1) — Live on android emulator-5554 from the pr-review state. e3-scene.log: 'SCENE e3 OK' then 'android.widget.Button Deleted line 68: version: '1.0.10',, tap to comment tappable [0,924][1080,982]' with 'Night mode: yes' (e3.png md5 f324966b16d0b4c60796987291193a21). e6-flip.log shows the same diff under '== set light =='/'Night mode: no' (e6-light.png md5 4a74c7d99a3a55b9953394b3098b1503) and under '== set dark =='/'Night mode: yes' (e6.png md5 f324966b16d0b4c60796987291193a21), so the same content repainted. e1-cloud-enter.log: 'session.sh: cloud-enter OK sid=ses_f5126a555fffSir2tNbx2eFsJe…

[e1] PR 6188 own scenarios on android: dark PR-review unified diff (e3), appearance flip with a diff on screen (e6), transcript code block (e1) — e2e-mobile-app/e6.png

[e1] PR 6188 own scenarios on android: dark PR-review unified diff (e3), appearance flip with a diff on screen (e6), transcript code block (e1)

[e1] PR 6188 own scenarios on android: dark PR-review unified diff (e3), appearance flip with a diff on screen (e6), transcript code block (e1) — e2e-mobile-app/e3.png

[e1] PR 6188 own scenarios on android: dark PR-review unified diff (e3), appeara -> pass :: Live on android emulator-5554 from the pr-review state. e3-scene.log: 'SCENE e3 OK' then 'android.widget.Button Deleted line 68: version: '1.0.10',, tap to comment tappable [0,924][1080,982]' with 'Night mode: yes' (e3.png md5 f324966b16d0b4c60796987291193a21). e6-flip.log shows the same diff under '== set light =='/'Night mode: no' (e6-light.png md5 4a74c7d99a3a55b9953394b3098b1503) and under '== set dark =='/'Night mode: yes' (e6.png md5 f324966b16d0b4c60796987291193a21), so the same content repainted. e1-cloud-enter.log: 'session.sh: cloud-enter OK sid=ses_f5126a555fffSir2tNbx2eFsJe device=emulator-5554 stages=create,enter,attach' (state created with the fake-LLM stub kilo/fake-determinis

Open findings (not fixed here)

  • [e1] Dark appearance: open an agent session transcript containing a fenced code block and confirm the highlighted tokens render the dark palette, unchanged from before this change.
  • [e1] $KILO_WORKFLOW/e2e/appium.sh script /home/igor_kilocode_ai/.local/share/kwf/sections/janitor-2026-09-15-experience-and-design-c99d/e2e-mobile-app/repro-e1.json --out /home/igor_kilocode_ai/.local/share/kwf/sections/janitor-2026-09-15-experience-and-design-c99d/e2e-mobile-app/repro-e1
  • [e1] spot check: no verdict: spot: no verdict — the frame got no usable verdict after one solo re-judge (evidence: /home/igor_kilocode_ai/.local/share/kwf/sections/janitor-2026-09-15-experience-and-design-c99d/e2e-mobile-app/e1-04.png)
  • not fully verified: some optional checks did not run
  • the '## E2E proof' section says the proof was not captured
e6-flip.mp4

pr-side-dark

pr-dark

e6-flip.mp4.trim.mp4

e3

e6

e6-light

e6

e3

e1

r1

e1-light

e6

e6-light

e1-diff-light

e1-session-light

e1

e3

e6

e6-light

e1

e3

e6-light

e3

e3-light

e6

e3

Surface: the mobile app (apps/mobile).

Surface: the mobile app (apps/mobile), iOS and Android. Area: dark mode and theming (no hardcoded colors).

# Problem
Three renderers decide which reference palette to paint by comparing a theme token to a hardcoded hex literal instead of reading the color scheme:

- `apps/mobile/src/components/agents/code-block.tsx:120` — `const isDark = colors.background === '#0E0E10';`
- `apps/mobile/src/components/pr-review/diff/diff-line.tsx:100` — `const isDark = colors.background === '#0E0E10';`
- `apps/mobile/src/components/pr-review/diff/pr-diff-side-by-side-row.tsx:208` — `const isDark = colors.background === '#0E0E10';`

The value is then passed to `tokenColorFor(className, isDark)` and to the `MUTED_COLOR` selection in `apps/mobile/src/lib/pr-review/diff/syntax-colors.ts` (which only has `light`/`dark` entries). The app's real dark signal is `useColorScheme()` in `apps/mobile/src/lib/hooks/use-theme-colors.ts` (`colorScheme === 'dark' ? darkColors : lightColors`), and other renderers already use it (`skeleton.tsx`, `markdown-text.tsx`, `markdown-table.tsx`). `darkColors.background` happens to be `'#0E0E10'` in `apps/mobile/src/lib/hooks/theme-colors.generated.ts` (generated from `src/global.css`), so the comparison is a coincidental equality: any change to that generated token silently flips code blocks and diff lines to the light syntax palette on a dark surface, producing an unreadable, wrong-theme diff.

# Requested behavior
- Determine l
@iscekic
iscekic marked this pull request as draft September 15, 2026 21:39
@kilo-code-bot

kilo-code-bot Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

All three production sites now derive isDark from useColorScheme(), which is the exact signal useThemeColors() already reads (apps/mobile/src/lib/hooks/use-theme-colors.ts:12), so both themes render the same palettes as before; no bugs, security, performance, or test-coverage problems were found in the eight changed files.

Verification notes:

  • Equivalence holds because only darkColors.background is #0E0E10 (lightColors.background is #FBFAF5), so colors.background === '#0E0E10' and useColorScheme() === 'dark' were already the same predicate; the hex literal now appears nowhere in src/.
  • The app-level theme override still flows through useColorScheme(): applyThemePreference calls Appearance.setColorScheme (apps/mobile/src/lib/hooks/use-theme-preference.ts:28-33), so the user preference and the system scheme stay on one signal.
  • Renderers re-render on an appearance flip because useColorScheme owns its own subscription; the memo wrappers are unaffected (SideColumn already compares isDark). No new subscriptions, listeners, or timers were added, so no memory-leak surface.
  • The new scheme-driven suites assert against tokenColorFor/MUTED_COLOR rather than hex literals, and the tests that mock react-native while rendering these components (tool-diff-preview.test.ts, markdown-renderer.test.ts) supply useColorScheme; DiffLine/SideBySideRow/CodeBlock are mocked or stubbed elsewhere, so no unmocked call site was found.
Files Reviewed (8 files)
  • apps/mobile/src/components/agents/code-block.tsx
  • apps/mobile/src/components/agents/code-block.test.ts
  • apps/mobile/src/components/agents/markdown-renderer.test.ts
  • apps/mobile/src/components/agents/tool-diff-preview.test.ts
  • apps/mobile/src/components/pr-review/diff/diff-line.tsx
  • apps/mobile/src/components/pr-review/diff/diff-line.mounted.test.tsx
  • apps/mobile/src/components/pr-review/diff/pr-diff-side-by-side-row.tsx
  • apps/mobile/src/components/pr-review/diff/pr-diff-side-by-side-row.mounted.test.tsx

Reviewed by deepseek-v4.1-flash · Input: 0 · Output: 0 · Cached: 0

Review guidance: REVIEW.md from base branch main

@iscekic
iscekic marked this pull request as ready for review September 15, 2026 21:51
@iscekic

iscekic commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

(bot) ## Live proof (proof-only request; no code change)

Ran on android; the request asked for android.

This request asked for live proof of existing behaviour. There is no diff; the verify round ran the must-run scenarios and recorded the evidence below.

[e1] kwf-fix proof-1bf7811 / PR 6188 — code block and PR-review diff render in light and dark on android — e2e-mobile-app/pr-side-dark.png
[e1] kwf-fix proof-1bf7811 / PR 6188 — code block and PR-review diff render in light and dark on android — e2e-mobile-app/pr-dark.png

[e1] kwf-fix proof-1bf7811 / PR 6188 — code block and PR-review diff render in l -> pass :: Android (emulator-5554): e1-flip.log reads 'Night mode: no' then 'SCENE cb-light OK', 'SCENE pr-light OK', 'SCENE pr-side-light OK', then 'Night mode: yes' then 'SCENE cb-dark OK', 'SCENE pr-dark OK', 'SCENE pr-side-dark OK'; the code-block fence text 'const alpha = 1;
const beta = 2;
export function total() { return alpha + beta; }' is in e1-code-dark.log, the unified rows 'Deleted line 8: return 1;' / 'Added line 8: // stub change' are in e1-light.log and e1-dark.log, and the side-by-side rows 'Old: return 1; | New: // stub change' and 'New only: export function alphaExtra() {' are in e1-light.log and e1-dark.log; captures cb-light/cb-dark.png, pr-light/pr-dark.png, pr-side-light/p
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/e1-flip.log
Night mode: no
SCENE cb-light OK
SCENE pr-light MISS assert 'PROFILE' (after 15s)
SCENE pr-side-light OK
SCENE pr-light OK
Night mode: yes
SCENE pr-dark OK
SCENE pr-side-dark OK
SCENE cb-dark OK
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/e1-code-dark.log
SCENE cb-dark OK
android.widget.LinearLayout com.kilocode.kiloapp:id/action_bar_root tappable [0,0][1080,2400]
android.widget.FrameLayout android:id/content tappable [0,0][1080,2400]
android.widget.Button Go back tappable [0,148][58,206]
android.widget.Button Rename session: TypeScript code snippet tappable [63,140][935,214]
android.view.View TypeScript code snippet tappable [63,158][935,195]
android.widget.Button Context 14 of 200,000 tokens, 0% used. Tap to view context details. tappable [951,144][1059,210]
android.widget.TextView 0% tappable [1021,167][1042,188]
android.widget.TextView 10:23 PM tappable [496,1879][583,1903]
android.view.ViewGroup fake:echo:Here is the snippet:, User message tappable [0,1914][1080,2105]
android.widget.TextView fake:echo:Here is the snippet: tappable [226,1935][1041,1971]
android.view.ViewGroup const alpha = 1;
const beta = 2;
export function total() { return alpha + beta; } tappable [244,1998][1023,2062]
android.widget.TextView const alpha = 1;
const beta = 2;
export function total() { return alpha + beta; } tappable [244,1998][950,2062]
android.widget.Button Preparation complete tappable [23,2118][1058,2177]
android.widget.TextView Preparation complete tappable [107,2133][291,2160]
android.view.ViewGroup Here, Assistant message tappable [0,2188][1080,2240]
android.widget.TextView Here tappable [21,2196][1059,2232]
android.widget.Button Add attachment tappable [16,2282][58,2324]
android.widget.EditText Message tappable [72,2270][920,2336]
android.widget.Button Start voice input tappable [940,2277][992,2329]
android.widget.Button Send message [992,2267][1064,2339]
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/e1-light.log
android.widget.TextView +6 tappable [174,371][192,391]
android.widget.TextView -1 tappable [203,371][218,391]
android.widget.CheckBox Mark src/alpha.ts as viewed tappable [1012,343][1059,391]
android.widget.Button Expand 20 lines of context tappable [393,420][564,441]
android.widget.TextView Expand 7 lines (1–7) tappable [416,420][564,441]
android.widget.Button Expand all 7 lines tappable [590,420][687,441]
android.widget.TextView Expand all tappable [613,420][687,441]
android.view.ViewGroup Hunk header @@ -8,4 +8,9 @@ export function alpha() { tappable [0,451][1080,487]
android.widget.TextView @@ -8,4 +8,9 @@ export function alpha() { tappable [21,456][1059,480]
android.view.ViewGroup Old:   return 1; | New:   // stub change tappable [0,487][1080,521]
android.widget.TextView 8 tappable [70,490][81,517]
android.widget.TextView   return 1; tappable [84,490][540,517]
android.widget.TextView 8 tappable [611,490][622,517]
android.widget.TextView   // stub change tappable [625,490][1080,517]
android.view.ViewGroup New only:   return 2; tappable [0,520][1080,554]
android.widget.TextView 9 tappable [611,523][622,550]
android.widget.TextView   return 2; tappable [625,523][1080,550]
android.view.ViewGroup New only: } tappable [0,553][1080,587]
android.widget.TextView 10 tappable [601,556][622,583]
android.widget.TextView } tappable [625,556][1080,583]
android.view.ViewGroup New only:  tappable [0,586][1080,620]
android.widget.TextView 11 tappable [601,589][622,616]
android.view.ViewGroup New only: export function alphaExtra() { tappable [0,619][1080,653]
android.widget.TextView 12 tappable [601,622][622,649]
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/e1-dark.log
android.widget.TextView +6 tappable [174,371][192,391]
android.widget.TextView -1 tappable [203,371][218,391]
android.widget.CheckBox Mark src/alpha.ts as viewed tappable [1012,343][1059,391]
android.widget.Button Expand 20 lines of context tappable [393,420][564,441]
android.widget.TextView Expand 7 lines (1–7) tappable [416,420][564,441]
android.widget.Button Expand all 7 lines tappable [590,420][687,441]
android.widget.TextView Expand all tappable [613,420][687,441]
android.view.ViewGroup Hunk header @@ -8,4 +8,9 @@ export function alpha() { tappable [0,451][1080,487]
android.widget.TextView @@ -8,4 +8,9 @@ export function alpha() { tappable [21,456][1059,480]
android.view.ViewGroup Old:   return 1; | New:   // stub change tappable [0,487][1080,521]
android.widget.TextView 8 tappable [70,490][81,517]
android.widget.TextView   return 1; tappable [84,490][540,517]
android.widget.TextView 8 tappable [611,490][622,517]
android.widget.TextView   // stub change tappable [625,490][1080,517]
android.view.ViewGroup New only:   return 2; tappable [0,520][1080,554]
android.widget.TextView 9 tappable [611,523][622,550]
android.widget.TextView   return 2; tappable [625,523][1080,550]
android.view.ViewGroup New only: } tappable [0,553][1080,587]
android.widget.TextView 10 tappable [601,556][622,583]
android.widget.TextView } tappable [625,556][1080,583]
android.view.ViewGroup New only:  tappable [0,586][1080,620]
android.widget.TextView 11 tappable [601,589][622,616]
android.view.ViewGroup New only: export function alphaExtra() { tappable [0,619][1080,653]
android.widget.TextView 12 tappable [601,622][622,649]
/home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/e1-shots.log
b2479339d64394f0eacc301ce30a5fd1  /home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/cb-light.png
a9cb97d92822093c9dffd654c6b77d86  /home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/cb-dark.png
584e9cb394032c73fb27f50f33f5646e  /home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/pr-light.png
762bf08707fe7c8517eda3452a2056c4  /home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/pr-dark.png
1679e0874e54dcc8501051b09585d3f9  /home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/pr-side-light.png
13d9fb2e5b369fc4e04d5e001b0becac  /home/igor_kilocode_ai/.local/share/kwf/sections/kwf-fix-proof-1bf7811-06af/e2e-mobile-app/pr-side-dark.png

@iscekic

iscekic commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

(bot) The live proof for this request is in the pull request description, under "## E2E proof".

This comment previously carried the capture paths on the machine that took them, so every image 404'd. Evidence now goes in the description, where the upload rewrites each reference.

@iscekic
iscekic marked this pull request as draft September 16, 2026 19:01
@iscekic
iscekic marked this pull request as ready for review September 16, 2026 19:08
@iscekic
iscekic marked this pull request as draft September 16, 2026 19:21
@iscekic
iscekic marked this pull request as ready for review September 16, 2026 19:24
@iscekic
iscekic marked this pull request as draft September 16, 2026 19:41
@iscekic
iscekic marked this pull request as ready for review September 16, 2026 19:48
@iscekic
iscekic marked this pull request as draft September 16, 2026 19:51
@iscekic
iscekic marked this pull request as ready for review September 16, 2026 19:56
@iscekic iscekic added the human-ready The PR is ready for human review. label Sep 17, 2026
@iscekic iscekic self-assigned this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants