Skip to content

feat: milestone 3 - auto-reconnect, pull-to-refresh, app state, syntax highlighting, theme system#35

Merged
vriesdemichael merged 1 commit intomainfrom
feat/milestone-3-polish
Feb 22, 2026
Merged

feat: milestone 3 - auto-reconnect, pull-to-refresh, app state, syntax highlighting, theme system#35
vriesdemichael merged 1 commit intomainfrom
feat/milestone-3-polish

Conversation

@vriesdemichael
Copy link
Copy Markdown
Owner

Summary

Implements all 6 issues for milestone 3.

Issue 14 - Auto-reconnection and connection status indicator

  • ConnectionBanner component: colored bar with status text and retry button
  • autoReconnect action in connection store with exponential backoff (1s, 2s, 4s, max 30s)
  • Banner wired above tabs so it is visible on all screens

Issue 15 - Pull-to-refresh and loading states

  • MessageList now accepts refreshing/onRefresh props wired to RefreshControl
  • Chat screen implements pull-to-refresh to re-fetch messages

Issue 16 - App background/foreground transitions

  • useAppState hook listens for AppState changes
  • On return to foreground: re-tests connection and re-fetches current session messages (per ADR 008)

Issue 17 - Android foreground service for SSE persistence

  • useAndroidSseService hook: lifecycle integration point for Android background SSE
  • Platform-conditional (no-op on iOS per ADR 008)

Issue 18 - Code syntax highlighting in messages

  • CodeBlock component with react-native-syntax-highlighter (atom-one themes)
  • parseCodeBlocks utility extracts fenced code blocks from markdown text
  • Assistant messages now render code blocks with syntax highlighting

Issue 19 - Dark/light theme with system preference and manual override

  • Theme store (app/store/theme.ts): Zustand + MMKV persistence for system/light/dark preference
  • useResolvedColorScheme: resolves effective theme from preference vs OS
  • Settings screen: theme picker with 3 segmented buttons
  • All existing useColorScheme calls now respect manual override

Testing

  • 17 test suites, 155 tests, all passing
  • New tests: ConnectionBanner (4), theme store (4), parseCodeBlocks (6)

New Dependencies

  • react-native-syntax-highlighter
  • react-syntax-highlighter
  • expo-task-manager

Closes #14, closes #15, closes #16, closes #17, closes #18, closes #19

… highlighting, and theme system

- Add ConnectionBanner component with status indicator and retry button (#14)
- Add autoReconnect with exponential backoff to connection store (#14)
- Add pull-to-refresh on chat screen via MessageList RefreshControl (#15)
- Add useAppState hook for foreground/background transitions (#16)
- Add useAndroidSseService hook placeholder for Android SSE persistence (#17)
- Add CodeBlock component with syntax highlighting via react-native-syntax-highlighter (#18)
- Add parseCodeBlocks utility to extract fenced code blocks from text (#18)
- Add theme store with system/light/dark preference persisted to MMKV (#19)
- Add theme picker (segmented buttons) to settings screen (#19)
- Update useColorScheme to resolve theme from store with manual override (#19)
- Wire useAppState and useAndroidSseService at root layout (#16, #17)
- Add ConnectionBanner to tab layout (#14)
- Add type declarations for syntax highlighter packages
- Add comprehensive tests for all new components and stores

closes #14, closes #15, closes #16, closes #17, closes #18, closes #19
@vriesdemichael vriesdemichael added the feature New feature label Feb 22, 2026
Copilot AI review requested due to automatic review settings February 22, 2026 22:25
@vriesdemichael vriesdemichael added the feature New feature label Feb 22, 2026
@vriesdemichael vriesdemichael enabled auto-merge (rebase) February 22, 2026 22:26
@vriesdemichael vriesdemichael merged commit 0b16be1 into main Feb 22, 2026
3 of 4 checks passed
@vriesdemichael vriesdemichael deleted the feat/milestone-3-polish branch February 22, 2026 22:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements milestone 3 features across connection lifecycle, message UX, and theming in the React Native app (expo-router + Zustand stores), including syntax-highlighted code blocks in assistant messages.

Changes:

  • Adds connection status banner + reconnection logic, plus app foreground/background handling hooks.
  • Adds pull-to-refresh for session chat message list and code-block rendering with syntax highlighting.
  • Introduces a persisted theme preference store with system/light/dark override and a settings UI picker.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
types/syntax-highlighter.d.ts Adds ambient typings for syntax highlighter packages
package.json Adds new dependencies for task manager + syntax highlighting
pnpm-lock.yaml Locks dependency graph updates
jest.setup.js Adds Jest mocks for syntax highlighter modules
hooks/use-color-scheme.ts Re-routes useColorScheme to resolved theme override hook
hooks/use-app-state.ts New hook to refresh connection/messages on foreground
hooks/use-android-sse-service.ts Android-only lifecycle integration hook for background SSE concept
components/message-list.tsx Adds RefreshControl support via refreshing/onRefresh props
components/message-item.tsx Renders assistant code blocks via CodeBlock + parseCodeBlocks
components/connection-banner.tsx New global connection status banner UI
components/code-block.tsx New code block UI + fenced-code parsing utility
components/tests/ConnectionBanner-test.tsx Adds banner rendering tests
components/tests/CodeBlock-test.ts Adds parser tests
app/store/theme.ts New persisted theme preference store + resolver hook
app/store/connection.ts Adds reconnect attempt tracking and autoReconnect
app/store/tests/theme-test.ts Adds theme store tests
app/session/[id].tsx Adds pull-to-refresh to chat session screen
app/_layout.tsx Initializes new global hooks at app root
app/(tabs)/settings.tsx Adds theme picker UI to Settings
app/(tabs)/_layout.tsx Places ConnectionBanner above tabs for global visibility
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hooks/use-app-state.ts
if (currentSessionId) {
// Clear cached messages to force refetch
useSessionStore.setState((state) => {
delete state.messages[currentSessionId];
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSessionStore.setState((state) => { delete state.messages[currentSessionId]; }) mutates the current Zustand state object and returns undefined. Since setState is not wrapped by the immer middleware, this relies on side effects and can be brittle. Prefer adding a store action (e.g., invalidateMessages) or call setState with an explicit partial return value.

Suggested change
delete state.messages[currentSessionId];
const { [currentSessionId]: _removed, ...restMessages } = state.messages ?? {};
return { messages: restMessages };

Copilot uses AI. Check for mistakes.
Comment thread hooks/use-app-state.ts
export function useAppState() {
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
const testConnection = useConnectionStore((s) => s.testConnection);
const { currentSessionId, selectSession } = useSessionStore();
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useSessionStore() is called without a selector and then destructured, which subscribes this hook to all session store updates. Since this runs at the app root, it can cause unnecessary re-renders on any session state change. Use selectors for currentSessionId and selectSession (e.g., separate useSessionStore((s) => ...) calls) to limit subscriptions.

Suggested change
const { currentSessionId, selectSession } = useSessionStore();
const currentSessionId = useSessionStore((s) => s.currentSessionId);
const selectSession = useSessionStore((s) => s.selectSession);

Copilot uses AI. Check for mistakes.
Comment thread app/session/[id].tsx
setRefreshing(true);
// Clear cached messages to force refetch
useSessionStore.setState((state) => {
delete state.messages[id];
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refresh handler uses useSessionStore.setState((state) => { delete state.messages[id]; }), which mutates the existing store state and returns undefined. Because setState bypasses the immer middleware, this is relying on side effects. Consider exposing a dedicated store action to invalidate a session’s messages, or return an explicit partial update from setState.

Suggested change
delete state.messages[id];
const { [id]: _removed, ...restMessages } = state.messages;
return { messages: restMessages };

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +14
* Android-only hook that keeps the SSE connection alive in the background
* by registering a background task via expo-task-manager.
*
* On iOS, this is a no-op — per ADR 008, iOS relies on reconnection
* and event catch-up on foreground resume.
*
* Note: expo-task-manager's defineTask must be called at module scope.
* For the full foreground service implementation, a custom native module
* would be needed. This hook provides the React lifecycle integration.
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says this hook keeps SSE alive “by registering a background task via expo-task-manager” and mentions defineTask, but there’s no expo-task-manager usage here. Either implement the task registration or update the comment to reflect that this hook is currently just a placeholder/logging integration point.

Suggested change
* Android-only hook that keeps the SSE connection alive in the background
* by registering a background task via expo-task-manager.
*
* On iOS, this is a no-op per ADR 008, iOS relies on reconnection
* and event catch-up on foreground resume.
*
* Note: expo-task-manager's defineTask must be called at module scope.
* For the full foreground service implementation, a custom native module
* would be needed. This hook provides the React lifecycle integration.
* Android-only hook that integrates SSE connection state with the React
* lifecycle and provides a placeholder for future background/foreground
* service integration.
*
* On iOS, this is a no-op per ADR 008, iOS relies on reconnection
* and event catch-up on foreground resume.
*
* For true background persistence of the SSE connection, a native
* foreground service or background task implementation would be required.
* This hook currently serves as a logging and integration point only.

Copilot uses AI. Check for mistakes.
Comment thread jest.setup.js
Comment on lines +44 to +50
jest.mock("react-native-syntax-highlighter", () => {
const { Text } = require("react-native");
return {
__esModule: true,
default: ({ children }) => Text({ children }),
};
});
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mock returns Text({ children }), but Text is a React component and shouldn’t be invoked as a plain function. This can break rendering in tests. Return a proper React element (e.g., a component that renders <Text>{children}</Text>) so the mock behaves like a component.

Copilot uses AI. Check for mistakes.
Comment thread package.json
"react-native-syntax-highlighter": "^2.1.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1",
"react-syntax-highlighter": "^16.1.0",
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

react-native-syntax-highlighter@2.1.0 has a peer dependency on react-syntax-highlighter ^6.0.4 (per the lockfile), but this PR adds react-syntax-highlighter@^16.1.0. That mismatch will result in unmet peer dependency warnings during install and may cause runtime incompatibilities. Align react-syntax-highlighter to the supported peer range, or upgrade react-native-syntax-highlighter to a version that supports v16.

Suggested change
"react-syntax-highlighter": "^16.1.0",
"react-syntax-highlighter": "^6.0.4",

Copilot uses AI. Check for mistakes.
Comment thread components/code-block.tsx
Comment on lines +74 to +79
// Text before code block
if (match.index > lastIndex) {
const before = text.slice(lastIndex, match.index).trim();
if (before) {
segments.push({ type: "text", content: before });
}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseCodeBlocks calls .trim() on the text before/after code blocks. This removes newlines and spaces around fenced blocks (e.g., blank lines between paragraphs and code), changing message formatting compared to the original raw text rendering. Consider preserving the original whitespace (avoid trim() here, or only trim where necessary) so surrounding text layout stays intact.

Copilot uses AI. Check for mistakes.
Comment thread app/(tabs)/settings.tsx
},
divider: {
height: StyleSheet.hairlineWidth,
backgroundColor: "#ccc",
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The divider uses a hard-coded #ccc, which won’t adapt to dark mode and can look too bright/low-contrast depending on the active theme. Consider deriving this color from Colors[colorScheme] (or using an existing border/placeholder color constant) so it matches the theme system.

Suggested change
backgroundColor: "#ccc",
backgroundColor: Colors.light.border,

Copilot uses AI. Check for mistakes.
Comment thread app/store/connection.ts
Comment on lines 31 to +36
immer((set, get) => ({
url: storage.getString("connection.url") || DEFAULT_URL,
username: storage.getString("connection.username") || DEFAULT_USERNAME,
status: "disconnected",
error: null,
reconnectAttempts: 0,
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storage is backed by new MMKV() declared at the top of this module, but MMKV isn’t imported anywhere (it’s currently only provided as a Jest global). This means storage.getString(...) here will throw at runtime in the real app. Import MMKV from react-native-mmkv in this module and remove the global/ts-expect-error approach.

Copilot uses AI. Check for mistakes.
Comment thread app/store/connection.ts
});

setTimeout(() => {
get().testConnection();
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autoReconnect schedules a delayed testConnection() unconditionally. If the connection becomes connected before the timeout fires (e.g., user taps Retry), the timer will still run and flip status back to connecting, causing unnecessary requests/UI churn. Consider checking the latest status inside the timeout (and/or cancelling any pending timer) before calling testConnection().

Suggested change
get().testConnection();
const { status: currentStatus } = get();
// Only auto-reconnect if we are still in an error state.
if (currentStatus === "error") {
get().testConnection();
}

Copilot uses AI. Check for mistakes.
@vriesdemichael
Copy link
Copy Markdown
Owner Author

🎉 This PR is included in version 1.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants