fix(mobile): address PR review comments and lint CI failure

- Exclude packages/mobile/** from root eslint config (mobile uses Expo/CJS toolchain)
- Align TERMINAL_STATUSES and NON_RESTORABLE_STATUSES with packages/core/src/types.ts
- Fix isRestorable() to use isTerminal() check matching core logic
- Change notification trigger from null to { seconds: 1 } for Android background task compat
- Add isPRRateLimited guard to merge button to prevent merge when API rate limited
- Await scheduleNotification in background task to ensure notifications are delivered

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
suraj-markup 2026-03-03 03:06:33 +05:30
parent 5a22a85e72
commit f3944b5f15
5 changed files with 22 additions and 16 deletions

View File

@ -15,6 +15,7 @@ export default tseslint.config(
"packages/web/postcss.config.mjs",
"test-clipboard*.mjs",
"test-clipboard*.sh",
"packages/mobile/**",
],
},

View File

@ -47,10 +47,10 @@ TaskManager.defineTask(TASK_ID, async () => {
const cooldownExpired = now - lastNotified > COOLDOWN_MS;
if (level === "respond" && (prev !== "respond" || cooldownExpired)) {
scheduleNotification(session, "respond");
await scheduleNotification(session, "respond");
timestamps[session.id] = now;
} else if (level === "merge" && (prev !== "merge" || cooldownExpired)) {
scheduleNotification(session, "merge");
await scheduleNotification(session, "merge");
timestamps[session.id] = now;
}
}

View File

@ -39,10 +39,10 @@ export async function setupNotifications(): Promise<boolean> {
}
/** Fire an immediate local notification for a session attention transition. */
export function scheduleNotification(
export async function scheduleNotification(
session: DashboardSession,
level: "respond" | "merge",
): void {
): Promise<void> {
const sessionLabel =
session.issueLabel ??
session.id;
@ -70,6 +70,9 @@ export function scheduleNotification(
...(Platform.OS === "android" && { channelId: ANDROID_CHANNEL_ID }),
};
// Fire immediately (trigger: null = instant)
void Notifications.scheduleNotificationAsync({ content, trigger: null });
// Use { seconds: 1 } instead of null — trigger: null fails silently on Android in background tasks
await Notifications.scheduleNotificationAsync({
content,
trigger: { seconds: 1, channelId: Platform.OS === "android" ? ANDROID_CHANNEL_ID : undefined },
});
}

View File

@ -22,6 +22,7 @@ import {
relativeTime,
isRestorable,
isTerminal,
isPRRateLimited,
ATTENTION_COLORS,
type DashboardCICheck,
type DashboardUnresolvedComment,
@ -155,7 +156,7 @@ export default function SessionDetailScreen({ route, navigation }: Props) {
const canRestore = isRestorable(session);
const isDone = isTerminal(session);
const pr = session.pr;
const isReadyToMerge = pr?.mergeability.mergeable && pr.state === "open";
const isReadyToMerge = pr?.mergeability.mergeable && pr.state === "open" && !isPRRateLimited(pr);
const failedChecks = pr?.ciChecks.filter((c) => c.status === "failed") ?? [];
const unresolvedComments = pr?.unresolvedComments ?? [];

View File

@ -119,26 +119,27 @@ export const ATTENTION_COLORS: Record<AttentionLevel, string> = {
done: "#8b949e",
};
/** Terminal states that cannot be restored */
const TERMINAL_STATUSES: SessionStatus[] = ["merged", "killed", "cleanup", "done", "terminated"];
/** Statuses that indicate the session is in a terminal (dead) state.
* Must stay in sync with packages/core/src/types.ts TERMINAL_STATUSES. */
const TERMINAL_STATUSES: SessionStatus[] = ["killed", "terminated", "done", "cleanup", "errored", "merged"];
const TERMINAL_ACTIVITIES: ActivityState[] = ["exited"];
const NON_RESTORABLE_STATUSES: SessionStatus[] = ["merged", "done", "terminated"];
/** Statuses that must never be restored (e.g. already merged).
* Must stay in sync with packages/core/src/types.ts NON_RESTORABLE_STATUSES. */
const NON_RESTORABLE_STATUSES: SessionStatus[] = ["merged"];
export function isTerminal(session: DashboardSession): boolean {
return (
TERMINAL_STATUSES.includes(session.status) ||
TERMINAL_ACTIVITIES.includes(session.activity as ActivityState)
(session.activity !== null && TERMINAL_ACTIVITIES.includes(session.activity))
);
}
export function isRestorable(session: DashboardSession): boolean {
return (
session.activity === "exited" &&
!NON_RESTORABLE_STATUSES.includes(session.status)
);
return isTerminal(session) && !NON_RESTORABLE_STATUSES.includes(session.status);
}
function isPRRateLimited(pr: DashboardPR): boolean {
export function isPRRateLimited(pr: DashboardPR): boolean {
return pr.mergeability.blockers.includes("API rate limited or unavailable");
}