Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6a1f225
feat(settings): sync selected skills and instructions across machines
saphid Oct 24, 2026
b80b309
fix(settings): keep fleet skill sync revisions and selections stable
saphid Oct 24, 2026
0b1a58f
fix(sync): recover saved state and avoid unnecessary bundle transfers
saphid Oct 24, 2026
50c4c81
fix(sync): preserve local edits and recover interrupted updates
saphid Oct 24, 2026
4db1357
fix(sync): protect instruction edits and surface destination conflicts
saphid Oct 24, 2026
874ebae
fix(sync): validate instruction installation and portable skill paths
saphid Oct 24, 2026
6e56820
fix(sync): align inventory limits and clean up setup receipts
saphid Oct 24, 2026
60777f9
fix(sync): normalize Windows instruction undo revisions
saphid Oct 24, 2026
0e2500e
fix(sync): validate refresh identity before journaling
saphid Oct 24, 2026
df0ae56
fix(sync): preserve subscriptions after unreadable state recovery
saphid Oct 24, 2026
30717ab
fix(sync): clean up unpublished subscription baselines
saphid Oct 24, 2026
4216b12
fix(sync): reconcile catalog publication before setup rollback
saphid Oct 24, 2026
b352cb1
fix(sync): finish admitted mutations across client disconnects
saphid Oct 24, 2026
d8aa4ec
feat(settings): browse skills and instruction files before sharing
saphid Oct 24, 2026
36e961d
fix(settings): retain sharing controls for unavailable files
saphid Oct 24, 2026
97f3ebd
fix(settings): refresh the open skill with its inventory
saphid Oct 24, 2026
df7f163
fix(settings): honor file scope and explicit sharing controls
saphid Oct 24, 2026
aaf0c2b
fix(settings): keep project location labels consistent
saphid Oct 24, 2026
6f746f4
Merge remote-tracking branch 'upstream/main' into feat/fleet-skills-i…
saphid Nov 4, 2026
8090a4c
fix(web): Skills & instructions is back in the settings sidebar
saphid Nov 4, 2026
8f8fbd0
chore: merge main into fleet skills instructions
maria-rcks Nov 4, 2026
2b346a0
fix(sync): explain lifecycle effects for lint
maria-rcks Nov 4, 2026
21ab4d5
chore: merge main into fleet skills instructions
maria-rcks Nov 4, 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
2 changes: 2 additions & 0 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AgentFileSyncCoordinator } from "./features/settings/AgentFileSyncCoordinator";
import { installPermissionUpdateNotice } from "./features/connection/permissionUpdateNotice";
import { installComposerAttachmentUploads } from "./state/composer-attachment-uploads";
import { installThreadOutboxDelivery } from "./state/threadOutboxDelivery";
Expand Down Expand Up @@ -233,6 +234,7 @@ function AppContent() {
<>
<SplashScreenCoordinator />
<SubscriptionUsageCoordinator />
<AgentFileSyncCoordinator />
<GestureHandlerRootView className="flex-1">
<KeyboardProvider statusBarTranslucent>
<SafeAreaProvider>
Expand Down
41 changes: 41 additions & 0 deletions apps/mobile/src/features/settings/AgentFileSyncCoordinator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { startAgentFileSyncRelay } from "@t3tools/client-runtime/state/agentFileSync";
import { EnvironmentId } from "@t3tools/contracts";
import { useEffect, useState } from "react";
import { AppState } from "react-native";
import { appAtomRegistry } from "../../state/atom-registry";
import { agentFileSync } from "../../state/agentFileSync";
import { useEnvironments } from "../../state/environments";

export function AgentFileSyncCoordinator() {
const { environments } = useEnvironments();
const [active, setActive] = useState(AppState.currentState === "active");
const key = environments
.filter(
(env) =>
env.connection.phase === "connected" &&
env.serverConfig?.environment.capabilities.agentFileSync,
)
.map((env) => env.environmentId)
.sort()
.join(",");
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Tracks native app activity so background clients stop relaying file changes.
useEffect(() => {
const subscription = AppState.addEventListener("change", (state) =>
setActive(state === "active"),
);
return () => subscription.remove();
}, []);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Keeps the sync relay running only while the app is active and these connections are mounted.
useEffect(
() =>
active
? startAgentFileSyncRelay(
appAtomRegistry,
agentFileSync,
key ? key.split(",").map((id) => EnvironmentId.make(id)) : [],
)
: undefined,
[active, key],
);
return null;
}
89 changes: 89 additions & 0 deletions apps/mobile/src/features/settings/AgentFileSyncSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useAtomValue } from "@effect/atom-react";
import { runAtomCommand, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
import type { EnvironmentId } from "@t3tools/contracts";
import { useEffect, useState } from "react";
import { View } from "react-native";
import { AppText as Text } from "../../components/ui/AppText";
import { appAtomRegistry } from "../../state/atom-registry";
import { agentFileSync } from "../../state/agentFileSync";
import { useEnvironmentQuery } from "../../state/query";
import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow";
import { SettingsSection } from "./components/SettingsSection";

const syncErrorMessage = (cause: unknown) =>
cause instanceof Error ? cause.message : "Could not update sync.";

export function AgentFileSyncSection({ environmentId }: { environmentId: EnvironmentId }) {
const links = useEnvironmentQuery(agentFileSync.links({ environmentId, input: {} }));
const allowed = useAtomValue(agentFileSync.manage.permissionAtom(environmentId));
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Owns the subscription refresh timer while these settings are mounted.
useEffect(() => {
const timer = setInterval(links.refresh, 15_000);
return () => clearInterval(timer);
}, [links.refresh]);
const manage = async (id: string, action: "pause" | "resume" | "remove") => {
setBusy(true);
setError(null);
try {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const result = await runAtomCommand(
appAtomRegistry,
agentFileSync.manage,
{ environmentId, input: { id, action } },
{ reportFailure: false },
);
if (result._tag === "Failure") {
setError(syncErrorMessage(squashAtomCommandFailure(result)));
}
links.refresh();
} catch (cause) {
setError(syncErrorMessage(cause));
}
setBusy(false);
};
return (
<SettingsSection
title="Skills & instructions"
footer="Choose skills and instruction sections in web or desktop Settings. Sync continues while a T3 client connects both machines. Stopping sync keeps the current files."
>
{(links.data ?? []).map((link) => (
<View key={link.id}>
<View className="gap-1 p-4">
<Text>
From {link.label} · {link.paused ? "Paused" : "Automatic"}
</Text>
<Text className="text-sm text-foreground-muted">
{link.scope.cwd ?? "Personal files"}
</Text>
{link.error ? (
<Text className="text-sm text-danger-foreground">{link.error}</Text>
) : null}
</View>
<SettingsActionRow
label={link.paused ? "Resume sync" : "Pause sync"}
disabled={busy || !allowed}
onPress={() => void manage(link.id, link.paused ? "resume" : "pause")}
/>
<SettingsActionRow
label="Stop sync"
disabled={busy || !allowed}
onPress={() => void manage(link.id, "remove")}
/>
</View>
))}
{!links.data?.length ? (
<View className="p-4">
<Text className="text-sm text-foreground-muted">
{links.error ?? "No automatic sync subscriptions."}
</Text>
</View>
) : null}
{error ? (
<View className="p-4">
<Text className="text-sm text-danger-foreground">{error}</Text>
</View>
) : null}
</SettingsSection>
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AgentFileSyncSection } from "./AgentFileSyncSection";
import * as Dialog from "../../components/ui/Dialog";
import { useAtom, useAtomValue } from "@effect/atom-react";
import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
Expand Down Expand Up @@ -230,6 +231,9 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
</Text>
) : null}
{notice ? <Text className="px-2 text-sm text-foreground-muted">{notice}</Text> : null}
{config?.environment.capabilities.agentFileSync && connected ? (
<AgentFileSyncSection environmentId={environmentId} />
) : null}
{config && !outdated ? (
<>
<SettingsSection title="T3 Code">
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/state/agentFileSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createAgentFileSyncAtoms } from "@t3tools/client-runtime/state/agentFileSync";
import { connectionAtomRuntime } from "../connection/runtime";
export const agentFileSync = createAgentFileSyncAtoms(connectionAtomRuntime);
Loading
Loading