Transform GitHub PR diffs into mobile-friendly Markdown β understand what changed per function without reading long code.
GitHub's mobile web renders code in a fixed-width monospace block. Long lines require horizontal scrolling, and deeply nested logic is impossible to read on a commute.
github-mobile-reader parses a git diff and produces a per-function summary β showing what each function/component added, removed, or changed rather than raw line diffs.
## π `src/components/TodoList.tsx`
> π‘ Previously fetched all tasks unconditionally. Now accepts filter and sortOrder
> params β fetchTasks is called conditionally and filter state drives re-fetching.
**Import changes**
+ `FilterBar`
+ `useSortedTasks`
- `LegacyLoader` (removed)
**βοΈ `TodoList`** _(Component)_ β changed
λ³μ: `filter`, `sortOrder`
**βοΈ `fetchTasks`** _(Function)_ β changed
νλΌλ―Έν°+ `filter`
νλΌλ―Έν°+ `sortOrder`
+ (API) `fetchTasks(filter)` β `tasks`
**β
`handleFilterChange`** _(Function)_ β added
νλΌλ―Έν°+ `field`
νλΌλ―Έν°+ `value`
+ (setState) `setFilter({...filter, [field]: value})`- Per-function summaries β each function/component gets its own line with status (added / removed / changed); no headings, normal font size on mobile
- Side-effect labels β behavior lines are prefixed with
(API),(setState),(state),(cond),(catch),(guard)so you can tell at a glance what kind of change it is - Guard clause detection β
if (!x) returnpatterns surfaced as(guard) early returnentries - Import changes β newly added or removed imports at the file level
- Parameter changes β added or removed function parameters
- Variables β simple variable assignments attached to the nearest function shown inline
- UI changes β added/removed JSX components (generic tags like
div,spanare filtered); map (π) and conditional (β‘) patterns - Props changes β TypeScript interface/type member changes (long string values abbreviated to
'...') - Gemini AI summaries (optional) β focuses on business logic change and side effects, not raw lines (
> π‘ ...) - Secure by default β tokens are injected via environment variables only; no flag that leaks to shell history
- CLI Usage
- GitHub Action
- Gemini AI Summaries (optional)
- Output Format
- npm Library Usage
- Language Support
- Project Structure
- Contributing
Run directly with npx β no setup or config file needed.
export GITHUB_TOKEN=ghp_xxxxSecurity note: The CLI does not accept a
--tokenflag. Passing secrets as CLI arguments exposes them in shell history andpsoutput.
npx github-mobile-reader --repo owner/repo --pr 42GEMINI_API_KEY=AIzaSy... npx github-mobile-reader --repo owner/repo --pr 42npx github-mobile-reader --repo owner/repo --all --limit 20| Flag | Default | Description |
|---|---|---|
--repo |
(required) | Repository in owner/repo format |
--pr |
β | Process a single PR by number |
--all |
β | Process all recent PRs (use with --limit) |
--out |
./reader-output |
Output directory β relative paths only, no .. |
--limit |
10 |
Max number of PRs to fetch when using --all |
--gemini-key |
β | Gemini API key (or set GEMINI_API_KEY env var) |
Token: read from $GITHUB_TOKEN (60 req/hr unauthenticated, 5,000 req/hr authenticated).
Each PR produces one file: reader-output/pr-<number>.md
Automatically generates a Reader document and posts a comment on every PR.
Create .github/workflows/mobile-reader.yml:
name: π Mobile Reader
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: write # commit the generated .md file
pull-requests: write # post the PR comment
jobs:
generate-reader:
name: Generate Mobile Reader View
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
base_branch: ${{ github.base_ref }}
output_dir: docs/reader
gemini_api_key: ${{ secrets.GEMINI_API_KEY }} # optional
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post PR Comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = './reader-output/pr-${{ github.event.pull_request.number }}.md';
if (!fs.existsSync(path)) { console.log('No reader file generated.'); return; }
const body = fs.readFileSync(path, 'utf8');
const comments = await github.rest.issues.listComments({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: ${{ github.event.pull_request.number }},
});
const prev = comments.data.find(c =>
c.user.login === 'github-actions[bot]' && c.body.startsWith('# π PR #')
);
if (prev) await github.rest.issues.deleteComment({
owner: context.repo.owner, repo: context.repo.repo, comment_id: prev.id,
});
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: ${{ github.event.pull_request.number }}, body,
});Every subsequent PR will automatically receive:
- A Reader Markdown file at
docs/reader/pr-<number>.md - A summary comment on the PR
| Input | Required | Default | Description |
|---|---|---|---|
github_token |
β | β | Use ${{ secrets.GITHUB_TOKEN }} |
base_branch |
β | main |
Base branch the PR is merging into |
output_dir |
β | docs/reader |
Directory for generated .md files |
gemini_api_key |
β | β | Gemini API key β omit to disable AI summaries |
Even complex hooks like useCanvasRenderer (200+ lines) get summarized in 1β3 sentences.
Without an API key, behavior is identical β no errors, no fallback output.
Uses Gemini 2.5 Flash Lite β fast, low-cost, no thinking overhead.
# via environment variable (recommended)
GEMINI_API_KEY=AIzaSy... npx github-mobile-reader --repo owner/repo --pr 42
# via flag
npx github-mobile-reader --repo owner/repo --pr 42 --gemini-key AIzaSy...- Go to Settings β Secrets and variables β Actions β New repository secret
- Name:
GEMINI_API_KEY, Value: your key - Add
gemini_api_key: ${{ secrets.GEMINI_API_KEY }}to the workflow (see example above)
Security: GitHub Secrets are masked in all workflow logs and never exposed in plain text.
# π PR #7 β feat: add task filtering and sort controls
> Repository: owner/repo
> Commit: `3f8a21c`
> Changed JS/TS files: 3
---
## π `src/components/TodoList.tsx`
> π‘ Previously fetched all tasks unconditionally. Now accepts filter and sortOrder
> params β fetchTasks is called conditionally and filter state drives re-fetching.
**Import changes**
+ `FilterBar`
+ `useSortedTasks`
- `LegacyLoader` (removed)
**βοΈ `TodoList`** _(Component)_ β changed
λ³μ: `filter`, `sortOrder`
+ (μν) `filter` β `useState({})`
+ `useEffect` [filter] λ³κ²½ μ μ€ν
**βοΈ `fetchTasks`** _(Function)_ β changed
νλΌλ―Έν°+ `filter`
νλΌλ―Έν°+ `sortOrder`
+ (guard) `!filter` β early return
+ (API) `api.getTasks(filter)` β `tasks`
**β
`handleFilterChange`** _(Function)_ β added
νλΌλ―Έν°+ `field`
νλΌλ―Έν°+ `value`
+ (setState) `setFilter({...filter, [field]: value})`
**βοΈ `TaskCard`** _(Component)_ β changed
Props+ `dueDate: '...'`
+ (cond) `!task.completed`
UI: `<Badge>`| Label | Meaning |
|---|---|
β
... β added |
Function/component newly introduced in the diff |
β ... β removed |
Function/component deleted in the diff |
βοΈ ... β changed |
Existing function/component with modified content |
λ³μ: x, y |
Simple variable assignments collapsed inline |
| Prefix | Meaning |
|---|---|
(API) |
await call β fetches data from a server or external service |
(setState) |
setState call β updates React state |
(state) |
Hook assignment β const x = useHook() |
(cond) |
if / else if branch |
(guard) |
Guard clause β if (!x) return early-exit pattern |
(catch) |
catch block |
(return) |
Non-trivial return value |
νλΌλ―Έν°+ / νλΌλ―Έν°- |
Function parameter added / removed |
Props+ / Props- |
TypeScript interface/type member added / removed |
UI: |
JSX component added or removed |
npm install github-mobile-readerimport { generateReaderMarkdown } from 'github-mobile-reader';
import { execSync } from 'child_process';
const diff = execSync('git diff HEAD~1 HEAD', { encoding: 'utf8' });
const markdown = generateReaderMarkdown(diff, {
pr: '42',
commit: 'a1b2c3d',
file: 'src/api/users.ts',
repo: 'my-org/my-repo',
});
console.log(markdown);import {
generateReaderMarkdown, // diff β complete Markdown document
parseDiffHunks, // diff β DiffHunk[]
attributeLinesToSymbols, // DiffHunk[] β SymbolDiff[]
generateSymbolSections, // SymbolDiff[] β string[]
extractImportChanges, // detect added/removed imports
extractParamChanges, // detect added/removed function parameters
} from 'github-mobile-reader';| Parameter | Type | Description |
|---|---|---|
diffText |
string |
Raw git diff output |
meta.pr |
string? |
Pull request number |
meta.commit |
string? |
Commit SHA |
meta.file |
string? |
File name |
meta.repo |
string? |
Repository in owner/repo format |
Returns: string β the complete Markdown document.
interface SymbolDiff {
name: string;
kind: 'component' | 'function' | 'setup';
status: 'added' | 'removed' | 'modified';
addedLines: string[];
removedLines: string[];
}The parser is optimized for JS/TS syntax patterns.
| Language | Extensions | Support |
|---|---|---|
| JavaScript | .js .mjs .cjs |
β Full |
| TypeScript | .ts |
β Full |
| React JSX | .jsx |
β Full |
| React TSX | .tsx |
β Full |
| Others | β | π Planned |
github-mobile-reader/
βββ src/
β βββ parser.ts β diff parsing and symbol analysis (core logic)
β βββ gemini.ts β Gemini 2.5 Flash Lite AI summaries (opt-in)
β βββ index.ts β public npm API
β βββ action.ts β GitHub Action entry point
β βββ cli.ts β CLI entry point
β βββ test.ts β smoke tests (npx ts-node src/test.ts)
βββ dist/ β compiled output (auto-generated)
βββ reader-output/ β CLI output directory (gitignored)
βββ action.yml β GitHub Action definition
βββ package.json
git clone https://github.com/3rdflr/github-mobile-reader.git
cd github-mobile-reader
npm install
npm run build:all # build library + Action + CLI
npx ts-node src/test.ts # run smoke testsPull requests are welcome.
MIT Β© 3rdflr