diff --git a/apps/mobile/src/MobileApp.tsx b/apps/mobile/src/MobileApp.tsx
index e931e26b..7b85b88e 100644
--- a/apps/mobile/src/MobileApp.tsx
+++ b/apps/mobile/src/MobileApp.tsx
@@ -34,6 +34,7 @@ import {
} from './mobileEditorSaveState'
import { applySavedMobileEditorDraft } from './mobileSavedDraftProjection'
import { MobileEditorAdapter } from './MobileEditorAdapter'
+import { MobileGitSyncStatusCard } from './MobileGitSyncStatusCard'
import {
createCompactNavigationState,
transitionCompactNavigation,
@@ -51,6 +52,8 @@ import { useMobileNoteDeleteFlow } from './useMobileNoteDeleteFlow'
import { useMobileNotePropertiesFlow } from './useMobileNotePropertiesFlow'
import { createNativeMobileAppStateStorage } from './mobileNativeAppStateStorage'
import { createNativeMobileVaultMetadataStorage } from './mobileNativeVaultMetadataStorage'
+import { createMobileGitSyncPlanForVault } from './mobileGitSyncRuntimePlan'
+import type { MobileGitSyncPlan } from './mobileGitSyncPlan'
import { defaultMobileVaultMetadata } from './mobileVaultMetadata'
import type { MobileVaultRuntime } from './mobileVaultRuntime'
import { useMobileVaultRuntimeLoader } from './useMobileVaultRuntimeLoader'
@@ -71,6 +74,10 @@ export function MobileApp() {
[availableNotes, compactNavigation.selectedNoteId],
)
const selectedSaveState = saveStateByNoteId[selectedNote.id] ?? idleMobileEditorSaveState
+ const gitSyncPlan = useMemo(
+ () => createMobileGitSyncPlanForVault({ vault: activeVaultMetadata }),
+ [activeVaultMetadata],
+ )
const autosaveQueue = useMemo(
() =>
createMobileAutosaveQueue({
@@ -140,6 +147,7 @@ export function MobileApp() {
void }) {
}
function NoteListPanel({
+ gitSyncPlan,
notes,
createNoteFailed,
createNoteTitle,
@@ -355,6 +368,7 @@ function NoteListPanel({
onSubmitCreateNote,
selectedNoteId,
}: {
+ gitSyncPlan: MobileGitSyncPlan
notes: MobileNote[]
createNoteFailed: boolean
createNoteTitle: string
@@ -378,6 +392,7 @@ function NoteListPanel({
} />
+
{runtimeLoadFailed ? : null}
+
+
+ {status.label}
+ {status.detail}
+
+ {status.actionLabel ? {status.actionLabel} : null}
+
+ )
+}
+
+function gitSyncToneStyle(tone: MobileGitSyncStatusTone) {
+ switch (tone) {
+ case 'attention':
+ return styles.gitSyncStatus_attention
+ case 'neutral':
+ return styles.gitSyncStatus_neutral
+ case 'positive':
+ return styles.gitSyncStatus_positive
+ case 'warning':
+ return styles.gitSyncStatus_warning
+ }
+}
diff --git a/apps/mobile/src/mobileGitSyncRuntimePlan.test.ts b/apps/mobile/src/mobileGitSyncRuntimePlan.test.ts
new file mode 100644
index 00000000..7af66b5a
--- /dev/null
+++ b/apps/mobile/src/mobileGitSyncRuntimePlan.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from 'vitest'
+import { createMobileGitSyncPlanForVault } from './mobileGitSyncRuntimePlan'
+
+describe('createMobileGitSyncPlanForVault', () => {
+ it('keeps app-local vaults hidden from Git sync status', () => {
+ expect(createMobileGitSyncPlanForVault({
+ vault: { id: 'personal', name: 'Personal Journal' },
+ })).toEqual({
+ primaryAction: null,
+ state: 'localOnly',
+ })
+ })
+
+ it('requires credentials for remote-backed vaults', () => {
+ expect(createMobileGitSyncPlanForVault({
+ vault: {
+ id: 'tolaria',
+ name: 'Tolaria',
+ remoteUrl: 'https://github.com/refactoringhq/tolaria.git',
+ },
+ })).toMatchObject({
+ authStrategy: 'githubOAuth',
+ host: 'github.com',
+ primaryAction: 'authenticate',
+ state: 'authRequired',
+ })
+ })
+
+ it('plans ready sync when credentials are available', () => {
+ expect(createMobileGitSyncPlanForVault({
+ credentials: { state: 'available' },
+ vault: {
+ id: 'tolaria',
+ name: 'Tolaria',
+ remoteUrl: 'git@git.example.com:refactoringhq/tolaria.git',
+ },
+ })).toMatchObject({
+ canPull: true,
+ canPush: false,
+ primaryAction: 'pull',
+ state: 'ready',
+ })
+ })
+
+ it('treats invalid remote metadata as local-only until vault management can repair it', () => {
+ expect(createMobileGitSyncPlanForVault({
+ vault: { id: 'broken', name: 'Broken', remoteUrl: '/Users/luca/Laputa' },
+ })).toEqual({
+ primaryAction: null,
+ state: 'localOnly',
+ })
+ })
+})
diff --git a/apps/mobile/src/mobileGitSyncRuntimePlan.ts b/apps/mobile/src/mobileGitSyncRuntimePlan.ts
new file mode 100644
index 00000000..f4c6c2bf
--- /dev/null
+++ b/apps/mobile/src/mobileGitSyncRuntimePlan.ts
@@ -0,0 +1,21 @@
+import { createMobileGitSyncPlan, type MobileGitCredentialState, type MobileGitSyncPlan } from './mobileGitSyncPlan'
+import { createMobileVaultConfig } from './mobileVaultConfig'
+import type { MobileVaultMetadata } from './mobileVaultMetadata'
+
+export function createMobileGitSyncPlanForVault({
+ credentials = { state: 'missing' },
+ vault,
+}: {
+ credentials?: MobileGitCredentialState
+ vault: MobileVaultMetadata
+}): MobileGitSyncPlan {
+ const result = createMobileVaultConfig(vault)
+ if (!result.ok) {
+ return { primaryAction: null, state: 'localOnly' }
+ }
+
+ return createMobileGitSyncPlan({
+ credentials,
+ sync: result.config.sync,
+ })
+}
diff --git a/apps/mobile/src/mobileGitSyncStatus.test.ts b/apps/mobile/src/mobileGitSyncStatus.test.ts
new file mode 100644
index 00000000..e90f2198
--- /dev/null
+++ b/apps/mobile/src/mobileGitSyncStatus.test.ts
@@ -0,0 +1,84 @@
+import { describe, expect, it } from 'vitest'
+import type { MobileGitRemote } from './mobileGitRemote'
+import { mobileGitSyncStatusView } from './mobileGitSyncStatus'
+
+describe('mobileGitSyncStatusView', () => {
+ it('hides local-only vaults from sync status chrome', () => {
+ expect(mobileGitSyncStatusView({ state: 'localOnly', primaryAction: null })).toBeNull()
+ })
+
+ it('summarizes authentication requirements by strategy', () => {
+ expect(mobileGitSyncStatusView({
+ authStrategy: 'githubOAuth',
+ host: 'github.com',
+ primaryAction: 'authenticate',
+ state: 'authRequired',
+ })).toMatchObject({
+ actionLabel: 'Connect',
+ detail: 'Connect GitHub for github.com.',
+ tone: 'attention',
+ })
+
+ expect(mobileGitSyncStatusView({
+ authStrategy: 'sshKey',
+ host: 'git.example.com',
+ primaryAction: 'authenticate',
+ state: 'authRequired',
+ })).toMatchObject({
+ detail: 'Add an SSH key for git.example.com.',
+ })
+ })
+
+ it.each([
+ { canPush: false, actionLabel: 'Pull', detail: 'Vault can pull from remote.', tone: 'positive' },
+ { canPush: true, actionLabel: 'Push', detail: 'Local changes are ready to push.', tone: 'warning' },
+ ] as const)('summarizes ready sync state with $actionLabel', ({ actionLabel, canPush, detail, tone }) => {
+ expect(mobileGitSyncStatusView({
+ canPull: true,
+ canPush,
+ primaryAction: canPush ? 'push' : 'pull',
+ remote: remote(),
+ state: 'ready',
+ })).toMatchObject({
+ actionLabel,
+ detail,
+ label: 'refactoringhq/tolaria',
+ tone,
+ })
+ })
+
+ it('summarizes syncing and failed operations', () => {
+ expect(mobileGitSyncStatusView({
+ operation: 'pull',
+ primaryAction: null,
+ remote: remote(),
+ state: 'syncing',
+ })).toMatchObject({
+ actionLabel: null,
+ detail: 'Pull in progress.',
+ tone: 'neutral',
+ })
+
+ expect(mobileGitSyncStatusView({
+ message: 'Authentication failed',
+ operation: 'push',
+ primaryAction: 'retry',
+ remote: remote(),
+ state: 'failed',
+ })).toMatchObject({
+ actionLabel: 'Retry',
+ detail: 'Authentication failed',
+ label: 'Push failed',
+ })
+ })
+})
+
+function remote(): MobileGitRemote {
+ return {
+ authStrategy: 'githubOAuth',
+ host: 'github.com',
+ owner: 'refactoringhq',
+ repository: 'tolaria',
+ url: 'https://github.com/refactoringhq/tolaria.git',
+ }
+}
diff --git a/apps/mobile/src/mobileGitSyncStatus.ts b/apps/mobile/src/mobileGitSyncStatus.ts
new file mode 100644
index 00000000..4792e043
--- /dev/null
+++ b/apps/mobile/src/mobileGitSyncStatus.ts
@@ -0,0 +1,55 @@
+import type { MobileGitSyncPlan } from './mobileGitSyncPlan'
+
+export type MobileGitSyncStatusTone = 'attention' | 'neutral' | 'positive' | 'warning'
+
+export type MobileGitSyncStatusView = {
+ actionLabel: string | null
+ detail: string
+ label: string
+ tone: MobileGitSyncStatusTone
+}
+
+export function mobileGitSyncStatusView(plan: MobileGitSyncPlan): MobileGitSyncStatusView | null {
+ switch (plan.state) {
+ case 'localOnly':
+ return null
+ case 'authRequired':
+ return {
+ actionLabel: 'Connect',
+ detail: authDetail(plan),
+ label: 'Git authentication required',
+ tone: 'attention',
+ }
+ case 'ready':
+ return {
+ actionLabel: actionLabel(plan.primaryAction),
+ detail: plan.canPush ? 'Local changes are ready to push.' : 'Vault can pull from remote.',
+ label: `${plan.remote.owner}/${plan.remote.repository}`,
+ tone: plan.canPush ? 'warning' : 'positive',
+ }
+ case 'syncing':
+ return {
+ actionLabel: null,
+ detail: `${actionLabel(plan.operation)} in progress.`,
+ label: `${plan.remote.owner}/${plan.remote.repository}`,
+ tone: 'neutral',
+ }
+ case 'failed':
+ return {
+ actionLabel: 'Retry',
+ detail: plan.message,
+ label: `${actionLabel(plan.operation)} failed`,
+ tone: 'warning',
+ }
+ }
+}
+
+function authDetail(plan: Extract) {
+ return plan.authStrategy === 'githubOAuth'
+ ? `Connect GitHub for ${plan.host}.`
+ : `Add an SSH key for ${plan.host}.`
+}
+
+function actionLabel(action: 'clone' | 'pull' | 'push') {
+ return action[0].toUpperCase() + action.slice(1)
+}
diff --git a/apps/mobile/src/styles.ts b/apps/mobile/src/styles.ts
index d291a361..acb3d532 100644
--- a/apps/mobile/src/styles.ts
+++ b/apps/mobile/src/styles.ts
@@ -1,6 +1,7 @@
import { commonStyles } from './styles/commonStyles'
import { editorSaveStateStyles } from './styles/editorSaveStateStyles'
import { editorStyles } from './styles/editorStyles'
+import { gitSyncStyles } from './styles/gitSyncStyles'
import { noteCreateStyles } from './styles/noteCreateStyles'
import { noteListStyles } from './styles/noteListStyles'
import { propertiesStyles } from './styles/propertiesStyles'
@@ -12,6 +13,7 @@ export const styles = {
...commonStyles,
...editorStyles,
...editorSaveStateStyles,
+ ...gitSyncStyles,
...noteCreateStyles,
...noteListStyles,
...propertiesStyles,
diff --git a/apps/mobile/src/styles/gitSyncStyles.ts b/apps/mobile/src/styles/gitSyncStyles.ts
new file mode 100644
index 00000000..364c0119
--- /dev/null
+++ b/apps/mobile/src/styles/gitSyncStyles.ts
@@ -0,0 +1,52 @@
+import { StyleSheet } from 'react-native'
+import { colors, spacing } from '../theme'
+
+export const gitSyncStyles = StyleSheet.create({
+ gitSyncStatus: {
+ minHeight: 54,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.sm,
+ marginHorizontal: spacing.lg,
+ marginTop: spacing.sm,
+ borderRadius: 8,
+ borderWidth: StyleSheet.hairlineWidth,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
+ },
+ gitSyncStatus_attention: {
+ borderColor: '#d99b57',
+ backgroundColor: '#fff4e6',
+ },
+ gitSyncStatus_neutral: {
+ borderColor: colors.border,
+ backgroundColor: colors.canvas,
+ },
+ gitSyncStatus_positive: {
+ borderColor: '#9bc49d',
+ backgroundColor: '#edf8ef',
+ },
+ gitSyncStatus_warning: {
+ borderColor: '#d8b15f',
+ backgroundColor: '#fff7dd',
+ },
+ gitSyncStatusAction: {
+ color: colors.primary,
+ fontSize: 13,
+ fontWeight: '800',
+ },
+ gitSyncStatusCopy: {
+ flex: 1,
+ minWidth: 0,
+ },
+ gitSyncStatusDetail: {
+ color: colors.textSoft,
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ gitSyncStatusLabel: {
+ color: colors.text,
+ fontSize: 13,
+ fontWeight: '800',
+ },
+})
diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md
index 6ee77b90..11f08c2b 100644
--- a/docs/MOBILE_PROGRESS.md
+++ b/docs/MOBILE_PROGRESS.md
@@ -8,7 +8,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
- Branch: `codex/mobile`
- Active phase: Phase 4 - Editor V1
-- Active slice: Core flow coverage and editor durability
+- Active slice: Git credential/auth boundaries and editor durability
- Push policy: commit locally; do not push unless explicitly requested
- Validation target: iPad/iOS simulator first
@@ -87,12 +87,13 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
- Added simple TenTap table serialization for rectangular tables, including entity decoding and escaped pipe characters, while continuing to block malformed table shapes.
- Reworked the mobile properties panel into expandable Type/Status/Icon/Tags picker rows that show current values first and reveal chip choices on demand.
- Added the first mobile Git sync plan model, covering local-only vaults, auth-required remotes, ready pull/push actions, active sync operations, and retryable failures.
+- Wired the mobile Git sync plan into the note-list UI through a visible status card for remote-backed vaults while local-only vaults remain chrome-free.
## Next Action
Continue Phase 4 with editor durability:
-1. Wire the Git sync plan into the mobile runtime/status UI, still behind model-only operations until the native git adapter is selected.
+1. Add credential storage/auth facades for GitHub OAuth and SSH key presence, still behind model-only Git operations until the native git adapter is selected.
2. Continue TenTap Markdown serialization coverage for any editor output observed in simulator QA.
3. Retry the iOS development-client build after installing an iOS 26.2 simulator runtime in Xcode.
@@ -322,6 +323,10 @@ Continue Phase 4 with editor durability:
- `pnpm --filter @tolaria/mobile typecheck` passed after mobile Git sync planning.
- CodeScene after mobile Git sync planning: `apps/mobile/src/mobileGitSyncPlan.ts` and `apps/mobile/src/mobileGitSyncPlan.test.ts` scored `10`.
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after mobile Git sync planning.
+- `pnpm --filter @tolaria/mobile test -- src/mobileGitSyncStatus.test.ts src/mobileGitSyncRuntimePlan.test.ts src/mobileGitSyncPlan.test.ts src/mobileVaultConfig.test.ts` passed after mobile Git sync status wiring: 31 files / 107 tests.
+- `pnpm --filter @tolaria/mobile typecheck` passed after mobile Git sync status wiring.
+- CodeScene after mobile Git sync status wiring: `apps/mobile/src/MobileApp.tsx`, `apps/mobile/src/MobileGitSyncStatusCard.tsx`, `apps/mobile/src/mobileGitSyncStatus.ts`, `apps/mobile/src/mobileGitSyncStatus.test.ts`, `apps/mobile/src/mobileGitSyncRuntimePlan.test.ts`, and `apps/mobile/src/styles/gitSyncStyles.ts` scored `10`; `apps/mobile/src/mobileGitSyncRuntimePlan.ts` and `apps/mobile/src/styles.ts` returned no scorable code and no findings.
+- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after mobile Git sync status wiring.
## Risks / Watch Items