Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f2d6926
fix(mobile): throw typed errors on Code Reviewer action failures
iscekic Jul 23, 2026
dd37fb4
fix(mobile): gate PR merge success on authoritative merged result
iscekic Jul 23, 2026
f9e0761
feat(web,app-shared): add field-merge Code Reviewer config patch endp…
iscekic Jul 23, 2026
325f9e0
fix(web,app-shared,mobile): decouple field-merge patch type from stri…
iscekic Jul 23, 2026
a735148
test(mobile): restore Wave-1 wiring coverage dropped by barrier refactor
iscekic Jul 23, 2026
9a6c479
fix(web,mobile): derive PR merge branch cleanup from authoritative PR…
iscekic Jul 23, 2026
58da932
feat(mobile): make Submit review reachable on Overview and Files
iscekic Jul 23, 2026
7c181e2
refactor(web,app-shared): share one bot fix-command parser
iscekic Jul 23, 2026
68e895c
fix(web): query PR review-thread reactions via reactionGroups
iscekic Jul 23, 2026
03a4cfb
fix(mobile): save Code Reviewer config via field-merge patch
iscekic Jul 23, 2026
ec06b38
fix(web): stop exposing GitLab webhook secret; add gated rotation
iscekic Jul 23, 2026
1caf10c
test(web): cover GitLab webhook rotation re-sync failure path
iscekic Jul 23, 2026
ca3586d
test(web): validate PR Review GraphQL documents against GitHub schema
iscekic Jul 23, 2026
ac6a762
fix: address Kilobot review comments on PR #4696
iscekic Jul 23, 2026
c1da718
merge: integrate origin/main into feature/mobile-audit-w1-pr-safety
iscekic Jul 27, 2026
79ef66c
test(web): cover CONVERSATION_COMMENTS_QUERY in GraphQL schema guard
iscekic Jul 27, 2026
bcf6c0f
docs(mobile): record PR-review E2E env traps in workflow learnings
iscekic Jul 27, 2026
9c30973
style(web): format resolved PR-review files with oxfmt
iscekic Jul 27, 2026
d74ec5b
merge: integrate origin/main (#4765 bot-skip config) into feature/mob…
iscekic Jul 27, 2026
3b5d1b8
chore: retrigger Kilo Code Review on the integrated head
iscekic Jul 27, 2026
20cbb2b
merge: integrate origin/main (docs-only conflict) into feature/mobile…
iscekic Jul 27, 2026
d700bf9
chore: retrigger Kilo Code Review on the integrated head
iscekic Jul 27, 2026
2b23c51
merge: integrate origin/main into feature/mobile-audit-w1-pr-safety
iscekic Jul 27, 2026
c010d01
merge: integrate origin/main (W1-B) into feature/mobile-audit-w1-pr-s…
iscekic Jul 27, 2026
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
17 changes: 17 additions & 0 deletions apps/mobile/.kilo/WORKFLOW_LEARNINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ Then wait event-driven with an `until grep -q EXITCODE= "$LOG"` loop that also b

## Orchestrator

### PR-review E2E env traps (stub token path)

- **Symptom:** `githubApps.devSeedUserGithubToken` fails or the app shows
`GitHub connection expired` even after seeding; the GitHub stub never logs
a request.
- **Cause:** (1) worktree `.env.local` missing the `USER_GITHUB_APP_TOKEN_*`
encryption keys, so the seeded token cannot be encrypted/decrypted; (2)
`services/git-token-service/.dev.vars` has empty token keys, so the token
endpoint returns 503.
- **Fix:** copy the missing `USER_GITHUB_APP_TOKEN_*` key lines from the
primary checkout's `.env.local` (temporary, strip after the run), fill the
empty keys in `services/git-token-service/.dev.vars`, restart
git-token-service + nextjs, re-seed, then reopen the PR in the app (a
"Check connection" retry alone may not refetch after the first 412s).
- **Also:** iOS shows an `Allow Paste` prompt before the PR-URL paste lands;
and the Safari `Open this page in "Kilo"?` wording can differ from the
settle-app regex — tap the exact `Open` accessibility action instead.
### iOS 26.5 scheme-confirmation prompt wording breaks login.sh on a fresh install

- Symptom: on a freshly installed dev client, `apps/mobile/e2e/login.sh` fails its settle assertion with the simulator stuck on the home screen under a SpringBoard dialog `Open in "Kilo"?` (Cancel/Open).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// P1-F-46b: the "Finish review" affordance on the Files tab must be
// reachable regardless of pending-comment count, so a clean PR (0
// queued comments) can still be approved. The downstream submit sheet
// + `buildSubmitReviewInput` already support a clean approve; see
// `src/lib/pr-review/build-submit-review-input.test.ts` for the
// builder coverage. This file only covers the Files-tab reachability
// wiring (button present + navigates to the submit route with the
// right params).
//
// Mutation-inversion gate: temporarily re-gating on
// `pending.items.length > 0` (or removing the button entirely) must
// make the "0 pending" case below FAIL.

import * as React from 'react';
import { describe, expect, it, vi } from 'vitest';

import { PrDiffFloatingActions } from './pr-diff-floating-actions';
import { type PendingReviewItem } from '@/lib/pr-review/pending-review-provider';
import { type SelectionState } from '@/lib/pr-review/diff-selection';

const routerPush = vi.fn();

vi.mock('expo-router', () => ({
useRouter: () => ({ push: routerPush }),
}));

vi.mock('react-native', () => ({
View: 'View',
Platform: { OS: 'ios' },
}));

vi.mock('lucide-react-native', () => ({
MessageCirclePlus: () => null,
}));

vi.mock('@/lib/hooks/use-theme-colors', () => ({
useThemeColors: () => ({
primaryForeground: '#FFFFFF',
foreground: '#000000',
mutedForeground: '#6F6A61',
}),
}));

vi.mock('@/components/ui/button', () => ({ Button: 'Button' }));
vi.mock('@/components/ui/text', () => ({ Text: 'Text' }));
vi.mock('@/lib/pr-review/diff-selection-bridge', () => ({
clearDiffSelection: vi.fn(),
}));

type PendingValue = {
items: PendingReviewItem[];
addComment: (item: PendingReviewItem) => void;
updateComment: (id: string, body: string) => void;
removeComment: (id: string) => void;
clear: () => void;
};

let currentPending: PendingValue = {
items: [],
addComment: vi.fn(() => undefined),
updateComment: vi.fn(() => undefined),
removeComment: vi.fn(() => undefined),
clear: vi.fn(() => undefined),
};

vi.mock('@/lib/pr-review/pending-review-provider', () => ({
usePendingReview: () => currentPending,
}));

const baseProps = {
owner: 'octocat',
repo: 'hello',
number: 7,
viewMode: 'unified' as const,
selection: null as SelectionState | null,
onClearSelection: vi.fn(),
};

type FindElementArgs = {
node: unknown;
type: string;
prop: string;
value: unknown;
};

function findElement({ node, type, prop, value }: FindElementArgs): React.ReactElement | null {
if (React.isValidElement(node)) {
const element = node;
const props = element.props as Record<string, unknown>;
if (element.type === type && props[prop] === value) {
return element;
}
const children = props.children;
if (Array.isArray(children)) {
for (const child of children) {
const found = findElement({ node: child, type, prop, value });
if (found) {
return found;
}
}
} else if (children !== undefined && children !== null) {
const found = findElement({ node: children, type, prop, value });
if (found) {
return found;
}
}
}
if (Array.isArray(node)) {
for (const child of node) {
const found = findElement({ node: child, type, prop, value });
if (found) {
return found;
}
}
}
return null;
}

function findSubmitButton() {
// eslint-disable-next-line new-cap
const element = PrDiffFloatingActions(baseProps);
return findElement({
node: element,
type: 'Button',
prop: 'accessibilityLabel',
value: 'Finish review',
});
}

function pressSubmit() {
const button = findSubmitButton();
if (!button) {
throw new Error('Finish review button not found in rendered tree');
}
const onPress = (button.props as { onPress?: () => void }).onPress;
onPress?.();
return button;
}

function makeItem(overrides: Partial<PendingReviewItem> = {}): PendingReviewItem {
return {
id: 'id-1',
path: 'src/lib.ts',
side: 'RIGHT',
line: 7,
body: 'Looks good.',
commitSha: 'head-1',
...overrides,
};
}

describe('PrDiffFloatingActions submit reachability (P1-F-46b)', () => {
function emptyPending(): PendingValue {
return {
items: [],
addComment: vi.fn(() => undefined),
updateComment: vi.fn(() => undefined),
removeComment: vi.fn(() => undefined),
clear: vi.fn(() => undefined),
};
}
function pendingWithItems(items: PendingReviewItem[]): PendingValue {
return {
items,
addComment: vi.fn(() => undefined),
updateComment: vi.fn(() => undefined),
removeComment: vi.fn(() => undefined),
clear: vi.fn(() => undefined),
};
}

it('renders the Finish review button when the queue is empty (clean PR)', () => {
currentPending = emptyPending();

const button = findSubmitButton();
expect(button).not.toBeNull();
});

it('renders the Finish review button when the queue has pending items', () => {
currentPending = pendingWithItems([makeItem(), makeItem({ id: 'id-2', path: 'src/other.ts' })]);

const button = findSubmitButton();
expect(button).not.toBeNull();
});

it('does not render a numeric count badge when the queue is empty', () => {
currentPending = emptyPending();

// Render the tree once to build the React element, then re-render
// and assert no child text node renders the number "0" inside the
// badge slot.
pressSubmit();
// eslint-disable-next-line new-cap
const tree = PrDiffFloatingActions(baseProps);
const serialized = JSON.stringify(tree);
expect(serialized).not.toContain('"text":"0"');
});

it('navigates to the review-submit route with owner/repo/number on press (clean PR)', () => {
currentPending = emptyPending();
routerPush.mockClear();

pressSubmit();

expect(routerPush).toHaveBeenCalledTimes(1);
expect(routerPush).toHaveBeenCalledWith({
pathname: '/(app)/pr-review/[owner]/[repo]/[number]/review-submit',
params: { owner: 'octocat', repo: 'hello', number: 7 },
});
});

it('navigates to the review-submit route with owner/repo/number on press (with pending)', () => {
currentPending = pendingWithItems([makeItem()]);
routerPush.mockClear();

pressSubmit();

expect(routerPush).toHaveBeenCalledTimes(1);
expect(routerPush).toHaveBeenCalledWith({
pathname: '/(app)/pr-review/[owner]/[repo]/[number]/review-submit',
params: { owner: 'octocat', repo: 'hello', number: 7 },
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// - The "Comment" affordance that pushes the comment-composer route
// when a diff-line selection exists, plus a "Clear" button that
// drops the selection.
// - The "Finish review" button shown when the pending review queue
// is non-empty, which pushes the review-submit route.
// - The "Finish review" button that pushes the review-submit route,
// shown regardless of pending-comment count so a clean PR can still
// be approved. The numeric count badge only renders when the queue
// is non-empty.
//
// Extracted from `pr-diff-file-list.tsx` to keep that file under the
// 300-line repo cap.
Expand Down Expand Up @@ -49,10 +51,10 @@ export function PrDiffFloatingActions({
const pending = usePendingReview();

const showSelectionAction = viewMode === 'unified' && selection !== null;
const showFinishReview = pending.items.length > 0;
if (!showSelectionAction && !showFinishReview) {
return null;
}
// P1-F-46b: the submit affordance must always be reachable from the
// Files tab, even when the pending-comment queue is empty (clean
// approve). The numeric count badge is only rendered when the queue
// is non-empty (see below), so a "0" never shows.

function openCommentComposer() {
if (!selection) {
Expand Down Expand Up @@ -113,20 +115,20 @@ export function PrDiffFloatingActions({
</Button>
</View>
) : null}
{showFinishReview ? (
<Button
onPress={openReviewSubmit}
accessibilityLabel="Finish review"
className={cn(showSelectionAction && 'mt-1')}
>
<View className="relative flex-row items-center">
<Text>Finish review</Text>
<Button
onPress={openReviewSubmit}
accessibilityLabel="Finish review"
className={cn(showSelectionAction && 'mt-1')}
>
<View className="relative flex-row items-center">
<Text>Finish review</Text>
{pending.items.length > 0 ? (
<View className="absolute -right-2.5 -top-2.5 min-h-5 min-w-5 items-center justify-center rounded-full bg-primary-foreground px-1.5">
<Text className="text-xs font-semibold text-primary">{pending.items.length}</Text>
</View>
</View>
</Button>
) : null}
) : null}
</View>
</Button>
</View>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it, vi } from 'vitest';

import { PrMergePartialSuccessBanner as banner } from './pr-merge-partial-success-banner';

vi.mock('react-native', () => ({
View: 'View',
}));

vi.mock('@/components/ui/text', () => ({
Text: 'Text',
}));

const REASON = 'Reference does not exist';

describe('PrMergePartialSuccessBanner', () => {
it('renders the merge-success headline and the branch-delete failure reason', () => {
const element = banner({ reason: REASON });
const serialized = JSON.stringify(element);

expect(serialized).toContain('Merged');
expect(serialized).toContain(`Couldn't delete the branch: ${REASON}`);
expect(serialized).toContain('polite');
});

it('contains NO Button or Pressable (no destructive CTA — there is nothing to retry or undo)', () => {
const element = banner({ reason: REASON });
const serialized = JSON.stringify(element);

expect(serialized).not.toContain('Button');
expect(serialized).not.toContain('Pressable');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Persistent partial-success banner surfaced on the PR review screen
// after a merge whose branch-delete step failed. The merge itself
// SUCCEEDED — this banner is informational only and intentionally has
// NO destructive CTA (the user already merged; re-running the merge
// would 422 and there is no rollback from the client side).
//
// Styling mirrors `AutoMergeEnabledBanner` so the two read as
// siblings; tone is "soft" / accent, not destructive.

import { View } from 'react-native';

import { Text } from '@/components/ui/text';

export function PrMergePartialSuccessBanner({ reason }: Readonly<{ reason: string }>) {
return (
<View
className="gap-1 rounded-lg bg-accent-soft p-4"
accessibilityLiveRegion="polite"
accessibilityLabel={`Merged. Couldn't delete the branch: ${reason}`}
>
<Text className="text-sm font-medium text-accent-soft-foreground">Merged</Text>
<Text className="text-xs text-accent-soft-foreground">
{`Couldn't delete the branch: ${reason}`}
</Text>
</View>
);
}
Loading
Loading